diff --git a/CHANGELOG/kelp-v0.3.2.md b/CHANGELOG/kelp-v0.3.2.md
new file mode 100644
index 00000000..ae8dae7f
--- /dev/null
+++ b/CHANGELOG/kelp-v0.3.2.md
@@ -0,0 +1,17 @@
+# v0.3.2
+> Release date: 19.03.2021
+
+**The regions update**:
+* Add basic region library to the kelp world library.
+ * Add supertype for regions `KelpRegion`
+ * Add region implementation `CuboidRegion` representing a cuboid area of blocks
+ * Add region implementation `EllipsoidRegion` representing ellipsoids including spheres and sphereoids
+ * Add new event type `KelpRegionEvent`
+ * Add `PlayerEnterRegionEvent` triggered when a player enters a region
+ * Add `PlayerLeaveRegionEvent` triggered when a player leaves a region
+* Add new multimap type `ConcurrentMultimap` offering multimaps with thread-safety.
+ * `ConcurrentSetMultimap` using a set in the background
+ * `ConcurrentListMultimap` using a normal list in the background
+* Add a custom implementation of bukkits block face: `KelpBlockFace`
+* Add documentation to `KelpChunk`
+* Add basic NPC and region tests to `testing-module`
\ No newline at end of file
diff --git a/README.md b/README.md
index 32d62a1c..b88d803a 100644
--- a/README.md
+++ b/README.md
@@ -125,13 +125,13 @@ There are version implementations for the following version implementations avai
com.github.pxav.kelp
core
- 0.3.1
+ 0.3.2
```
### Gradle
```shell script
-compile group: 'com.github.pxav.kelp', name: 'core', version: '0.3.1'
+compile group: 'com.github.pxav.kelp', name: 'core', version: '0.3.2'
```
### Other build tools
diff --git a/core/pom.xml b/core/pom.xml
index 1890182d..0fccfd18 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -5,7 +5,7 @@
com.github.pxav.kelp
parent
- 0.3.1
+ 0.3.2
4.0.0
diff --git a/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java b/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java
index 0b8a048d..bf61ba98 100644
--- a/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java
+++ b/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java
@@ -35,7 +35,7 @@
*
* @author pxav
*/
-@Plugin(name = "Kelp", version = "0.3.1")
+@Plugin(name = "Kelp", version = "0.3.2")
@Author("pxav")
@Description("A cross version spigot framework.")
@Singleton
diff --git a/core/src/main/java/de/pxav/kelp/core/common/ConcurrentListMultimap.java b/core/src/main/java/de/pxav/kelp/core/common/ConcurrentListMultimap.java
new file mode 100644
index 00000000..f35e7a91
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/common/ConcurrentListMultimap.java
@@ -0,0 +1,222 @@
+package de.pxav.kelp.core.common;
+
+import com.google.common.collect.*;
+
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class ConcurrentListMultimap implements ConcurrentMultimap {
+
+ private final ConcurrentMap> map;
+
+ public static ConcurrentListMultimap create() {
+ return new ConcurrentListMultimap<>();
+ }
+
+ public static ConcurrentListMultimap create(Multimap source) {
+ ConcurrentListMultimap multimap = new ConcurrentListMultimap<>();
+ multimap.putAll(source);
+ return multimap;
+ }
+
+ public static ConcurrentListMultimap create(Map source) {
+ ConcurrentListMultimap multimap = new ConcurrentListMultimap<>();
+ source.forEach(multimap::put);
+ multimap.putAll(source);
+ return multimap;
+ }
+
+ public ConcurrentListMultimap() {
+ this.map = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (Collection value : this.map.values()) {
+ size += value.size();
+ }
+ return size;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.map.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object o) {
+ return this.map.containsKey(o);
+ }
+
+ @Override
+ public boolean containsValue(Object o) {
+ Iterator> iterator = this.asMap().values().iterator();
+
+ Collection collection;
+
+ do {
+ if (!iterator.hasNext()) {
+ return false;
+ }
+
+ collection = iterator.next();
+ } while(!collection.contains(o));
+
+ return true;
+ }
+
+ @Override
+ public boolean containsEntry(Object o, Object o1) {
+ Collection collection = this.map.get(o);
+ return collection != null && collection.contains(o1);
+ }
+
+ @Override
+ public boolean put(K k, V v) {
+ if (this.get(k) == null) {
+ this.map.put(k, Lists.newArrayList());
+ }
+ return this.get(k).add(v);
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ if (this.map.get(key) == null) {
+ return false;
+ }
+ return this.map.get(key).remove(value);
+ }
+
+ @Override
+ public boolean putAll(K k, Iterable extends V> iterable) {
+ if (iterable instanceof Collection) {
+ Collection collection = (Collection) iterable;
+ if (collection.isEmpty()) {
+ return false;
+ }
+
+ if (this.get(k) == null) {
+ this.map.put(k, Lists.newArrayList());
+ }
+ return this.get(k).addAll(collection);
+ } else {
+ Iterator extends V> valueItr = iterable.iterator();
+ if (!valueItr.hasNext()) {
+ return false;
+ }
+
+ if (this.get(k) == null) {
+ this.map.put(k, Lists.newArrayList());
+ }
+ return Iterators.addAll(this.get(k), valueItr);
+ }
+ }
+
+ @Override
+ public boolean putAll(Multimap extends K, ? extends V> multimap) {
+ for (Map.Entry extends K, ? extends V> entry : multimap.entries()) {
+ this.put(entry.getKey(), entry.getValue());
+ }
+ return false;
+ }
+
+ @Override
+ public void putAll(Map newMap) {
+ for (Map.Entry entry : newMap.entrySet()) {
+ this.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public Collection replaceValues(K k, Iterable extends V> iterable) {
+ Collection result = this.removeAll(k);
+ this.putAll(k, iterable);
+ return result;
+ }
+
+ @Override
+ public Collection removeAll(Object key) {
+ if (this.map.get(key) == null) {
+ return Lists.newArrayList();
+ }
+ return this.map.remove(key);
+ }
+
+ @Override
+ public void clear() {
+ this.map.clear();
+ }
+
+ @Override
+ public Collection get(@Nullable K k) {
+ return this.map.get(k);
+ }
+
+ @Override
+ public Set keySet() {
+ return this.map.keySet();
+ }
+
+ @Override
+ public Multiset keys() {
+ return HashMultiset.create(this.map.keySet());
+ }
+
+ @Override
+ public Collection values() {
+ Iterator> iterator = this.asMap().values().iterator();
+ Collection result = Lists.newArrayList();
+
+ while (iterator.hasNext()) {
+ result.addAll(iterator.next());
+ }
+
+ return result;
+ }
+
+ @Override
+ public Collection> entries() {
+ Collection> entries = Lists.newArrayList();
+ this.map.forEach((key, valueSet) -> valueSet.forEach(element -> {
+ AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry<>(key, element);
+ entries.add(entry);
+ }));
+ return entries;
+ }
+
+
+ @Override
+ public ConcurrentMap> asMap() {
+ ConcurrentMap> output = Maps.newConcurrentMap();
+ output.putAll(this.map);
+ return output;
+ }
+
+ @Override
+ public boolean containsValue(Collection iterable) {
+ return this.map.containsValue(iterable);
+ }
+
+ @Override
+ public Collection getOrDefault(K key, Collection defaultCollection) {
+ return this.map.getOrDefault(key, Lists.newArrayList(defaultCollection));
+ }
+
+ @Override
+ public Collection getOrEmpty(K key) {
+ return this.map.getOrDefault(key, Lists.newArrayList());
+ }
+
+ @Override
+ public void removeWithValue(V value) {
+ this.map.forEach((key, valueSet) -> valueSet.forEach(current -> {
+ if (current.equals(value)) {
+ this.remove(key, current);
+ }
+ }));
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/common/ConcurrentMultimap.java b/core/src/main/java/de/pxav/kelp/core/common/ConcurrentMultimap.java
new file mode 100644
index 00000000..06dce0d7
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/common/ConcurrentMultimap.java
@@ -0,0 +1,67 @@
+package de.pxav.kelp.core.common;
+
+import com.google.common.collect.Multimap;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * This is a custom implementation of Guava's normal {@link Multimap} with the
+ * only difference that this offers thread-safety and some performance optimizations
+ * in some operations, while other operations are slightly slower than with a normal
+ * {@code Multimap}.
+ *
+ * @param The key type for each multimap entry
+ * @param The value type for each multimap entry.
+ */
+public interface ConcurrentMultimap extends Multimap {
+
+ /**
+ * Checks whether a single key has all values
+ * contained by the given collection.
+ *
+ * @param iterable The collection to check.
+ * @return {@code true} of a single key had all of the values contained by the collection.
+ */
+ boolean containsValue(Collection iterable);
+
+ /**
+ * Gets the collection associated with the given
+ * key. If there is no entry for the given key,
+ * the given fallback collection will be returned.
+ *
+ * @param key The key to get the collection of.
+ * @param defaultCollection The fallback collection to return
+ * if there is no collection associated with the given key.
+ * @return The collection associated with the given key or the given fallback collection.
+ */
+ Collection getOrDefault(K key, Collection defaultCollection);
+
+ /**
+ * Gets the collection associated with the given
+ * key. If there is no entry for the given key,
+ * an empty collection (depending on the implementation)
+ * will be returned.
+ *
+ * @param key The key to get the collection of.
+ * @return The collection associated with the given key or an empty collection.
+ */
+ Collection getOrEmpty(K key);
+
+ /**
+ * Removes all entries with the given value.
+ *
+ * @param value The value to remove all entries with.
+ */
+ void removeWithValue(V value);
+
+ /**
+ * Takes a normal map and inserts its values
+ * into the multimap. If one of the contained keys
+ * already exists, it will simply be added to the collection.
+ *
+ * @param newMap The map to be added to the multimap.
+ */
+ void putAll(Map newMap);
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/common/ConcurrentSetMultimap.java b/core/src/main/java/de/pxav/kelp/core/common/ConcurrentSetMultimap.java
new file mode 100644
index 00000000..55c3b787
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/common/ConcurrentSetMultimap.java
@@ -0,0 +1,221 @@
+package de.pxav.kelp.core.common;
+
+import com.google.common.collect.*;
+
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class ConcurrentSetMultimap implements ConcurrentMultimap {
+
+ private final ConcurrentMap> map;
+
+ public static ConcurrentSetMultimap create() {
+ return new ConcurrentSetMultimap<>();
+ }
+
+ public static ConcurrentSetMultimap create(Multimap source) {
+ ConcurrentSetMultimap multimap = new ConcurrentSetMultimap<>();
+ multimap.putAll(source);
+ return multimap;
+ }
+
+ public static ConcurrentSetMultimap create(Map source) {
+ ConcurrentSetMultimap multimap = new ConcurrentSetMultimap<>();
+ source.forEach(multimap::put);
+ multimap.putAll(source);
+ return multimap;
+ }
+
+ public ConcurrentSetMultimap() {
+ this.map = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (Collection value : this.map.values()) {
+ size += value.size();
+ }
+ return size;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.map.isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object o) {
+ return this.map.containsKey(o);
+ }
+
+ @Override
+ public boolean containsValue(Object o) {
+ Iterator> iterator = this.asMap().values().iterator();
+
+ Collection collection;
+
+ do {
+ if (!iterator.hasNext()) {
+ return false;
+ }
+
+ collection = iterator.next();
+ } while(!collection.contains(o));
+
+ return true;
+ }
+
+ @Override
+ public boolean containsEntry(Object o, Object o1) {
+ Collection collection = this.map.get(o);
+ return collection != null && collection.contains(o1);
+ }
+
+ @Override
+ public boolean put(K k, V v) {
+ if (this.get(k) == null) {
+ this.map.put(k, Sets.newHashSet());
+ }
+ return this.get(k).add(v);
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ if (this.map.get(key) == null) {
+ return false;
+ }
+ return this.map.get(key).remove(value);
+ }
+
+ @Override
+ public boolean putAll(K k, Iterable extends V> iterable) {
+ if (iterable instanceof Collection) {
+ Collection collection = (Collection) iterable;
+ if (collection.isEmpty()) {
+ return false;
+ }
+
+ if (this.get(k) == null) {
+ this.map.put(k, Sets.newHashSet());
+ }
+ return this.get(k).addAll(collection);
+ } else {
+ Iterator extends V> valueItr = iterable.iterator();
+ if (!valueItr.hasNext()) {
+ return false;
+ }
+
+ if (this.get(k) == null) {
+ this.map.put(k, Sets.newHashSet());
+ }
+ return Iterators.addAll(this.get(k), valueItr);
+ }
+ }
+
+ @Override
+ public boolean putAll(Multimap extends K, ? extends V> multimap) {
+ for (Map.Entry extends K, ? extends V> entry : multimap.entries()) {
+ this.put(entry.getKey(), entry.getValue());
+ }
+ return false;
+ }
+
+ @Override
+ public void putAll(Map newMap) {
+ for (Map.Entry entry : newMap.entrySet()) {
+ this.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public Collection replaceValues(K k, Iterable extends V> iterable) {
+ Collection result = this.removeAll(k);
+ this.putAll(k, iterable);
+ return result;
+ }
+
+ @Override
+ public Collection removeAll(Object key) {
+ if (this.map.get(key) == null) {
+ return Lists.newArrayList();
+ }
+ return this.map.remove(key);
+ }
+
+ @Override
+ public void clear() {
+ this.map.clear();
+ }
+
+ @Override
+ public Collection get(@Nullable K k) {
+ return this.map.get(k);
+ }
+
+ @Override
+ public Set keySet() {
+ return this.map.keySet();
+ }
+
+ @Override
+ public Multiset keys() {
+ return HashMultiset.create(this.map.keySet());
+ }
+
+ @Override
+ public Collection values() {
+ Iterator> iterator = this.asMap().values().iterator();
+ Collection result = Lists.newArrayList();
+
+ while (iterator.hasNext()) {
+ result.addAll(iterator.next());
+ }
+
+ return result;
+ }
+
+ @Override
+ public Collection> entries() {
+ Collection> entries = Sets.newHashSet();
+ this.map.forEach((key, valueSet) -> valueSet.forEach(element -> {
+ AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry<>(key, element);
+ entries.add(entry);
+ }));
+ return entries;
+ }
+
+
+ @Override
+ public ConcurrentMap> asMap() {
+ ConcurrentMap> output = Maps.newConcurrentMap();
+ output.putAll(this.map);
+ return output;
+ }
+
+ @Override
+ public boolean containsValue(Collection iterable) {
+ return this.map.containsValue(iterable);
+ }
+
+ @Override
+ public Set getOrDefault(K key, Collection defaultCollection) {
+ return this.map.getOrDefault(key, Sets.newHashSet(defaultCollection));
+ }
+
+ @Override
+ public Set getOrEmpty(K key) {
+ return this.map.getOrDefault(key, Sets.newHashSet());
+ }
+
+ @Override
+ public void removeWithValue(V value) {
+ this.map.forEach((key, valueSet) -> valueSet.forEach(current -> {
+ if (current.equals(value)) {
+ this.remove(key, current);
+ }
+ }));
+ }
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/KelpRegionEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/KelpRegionEvent.java
new file mode 100644
index 00000000..c989345e
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/KelpRegionEvent.java
@@ -0,0 +1,32 @@
+package de.pxav.kelp.core.event.kelpevent.region;
+
+import de.pxav.kelp.core.world.region.KelpRegion;
+import org.bukkit.event.Event;
+
+/**
+ * A region event is any event that is related to a specific {@link KelpRegion}.
+ *
+ * In most cases you first have to enable listeners for your region using
+ * {@link KelpRegion#enableListeners()} to work with this region.
+ *
+ * @author pxav
+ */
+public abstract class KelpRegionEvent extends Event {
+
+ // the region handled by this event
+ protected KelpRegion region;
+
+ public KelpRegionEvent(KelpRegion region) {
+ this.region = region;
+ }
+
+ /**
+ * Gets the region involved in the current event.
+ *
+ * @return The region involved in this event.
+ */
+ public KelpRegion getRegion() {
+ return region;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/PlayerEnterRegionEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/PlayerEnterRegionEvent.java
new file mode 100644
index 00000000..f1baff09
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/PlayerEnterRegionEvent.java
@@ -0,0 +1,45 @@
+package de.pxav.kelp.core.event.kelpevent.region;
+
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.world.region.KelpRegion;
+import org.bukkit.event.HandlerList;
+
+/**
+ * This event is triggered when a player enters a {@link KelpRegion}
+ * that has listeners enabled.
+ *
+ * The opposite of this event would be {@link PlayerLeaveRegionEvent}
+ *
+ * @author pxav
+ */
+public class PlayerEnterRegionEvent extends KelpRegionEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+
+ // player who entered the region
+ private KelpPlayer player;
+
+ public PlayerEnterRegionEvent(KelpRegion region, KelpPlayer player) {
+ super(region);
+ this.player = player;
+ }
+
+ /**
+ * Gets the player who entered the region.
+ *
+ * @return The player who entered the region.
+ */
+ public KelpPlayer getPlayer() {
+ return player;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/PlayerLeaveRegionEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/PlayerLeaveRegionEvent.java
new file mode 100644
index 00000000..a9341101
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/region/PlayerLeaveRegionEvent.java
@@ -0,0 +1,44 @@
+package de.pxav.kelp.core.event.kelpevent.region;
+
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.world.region.KelpRegion;
+import org.bukkit.event.HandlerList;
+
+/**
+ * This event is triggered when a player leaves a {@link KelpRegion}
+ * that has listeners enabled.
+ *
+ * The opposite of this event would be {@link PlayerEnterRegionEvent}
+ *
+ * @author pxav
+ */
+public class PlayerLeaveRegionEvent extends KelpRegionEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+
+ private KelpPlayer player;
+
+ public PlayerLeaveRegionEvent(KelpRegion region, KelpPlayer player) {
+ super(region);
+ this.player = player;
+ }
+
+ /**
+ * Gets the player who left the region.
+ *
+ * @return The player who left the region.
+ */
+ public KelpPlayer getPlayer() {
+ return player;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/KelpBlock.java b/core/src/main/java/de/pxav/kelp/core/world/KelpBlock.java
index 2ec689d1..9f72e83a 100644
--- a/core/src/main/java/de/pxav/kelp/core/world/KelpBlock.java
+++ b/core/src/main/java/de/pxav/kelp/core/world/KelpBlock.java
@@ -1,11 +1,14 @@
package de.pxav.kelp.core.world;
+import com.google.common.base.Objects;
import de.pxav.kelp.core.KelpPlugin;
import de.pxav.kelp.core.inventory.material.KelpMaterial;
import de.pxav.kelp.core.world.util.CardinalDirection;
+import de.pxav.kelp.core.world.util.KelpBlockFace;
import de.pxav.kelp.core.world.version.BlockVersionTemplate;
+import org.apache.commons.lang.builder.HashCodeBuilder;
import org.bukkit.block.Block;
-import org.bukkit.block.BlockFace;
+import org.bukkit.craftbukkit.v1_16_R3.block.CraftBlock;
import org.bukkit.util.Vector;
/**
@@ -275,9 +278,10 @@ public void setMaterial(KelpMaterial material) {
* might grow to a tree or grass might spawn random flowers
* and so on.
*
- * This method by default applies the bone meal on the upper side of a block. */
+ * This method by default applies the bone meal on the upper side of a block.
+ */
public void applyBoneMeal() {
- versionTemplate.applyBoneMeal(this, BlockFace.UP);
+ versionTemplate.applyBoneMeal(this, KelpBlockFace.UP);
}
/**
@@ -288,7 +292,7 @@ public void applyBoneMeal() {
*
* @param blockFace The face of the block to apply the bone meal on.
*/
- public void applyBoneMeal(BlockFace blockFace) {
+ public void applyBoneMeal(KelpBlockFace blockFace) {
versionTemplate.applyBoneMeal(this, blockFace);
}
@@ -315,4 +319,31 @@ public Block getBukkitBlock() {
return bukkitBlock;
}
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+
+ if (!(object instanceof KelpBlock)) {
+ return false;
+ }
+
+ KelpBlock kelpBlock = (KelpBlock) object;
+ return kelpBlock.getX() == this.getX()
+ && kelpBlock.getY() == this.getY()
+ && kelpBlock.getZ() == this.getZ()
+ && kelpBlock.getWorldName().equalsIgnoreCase(this.getWorldName());
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(this.getWorldName())
+ .append(getX())
+ .append(getY())
+ .append(getZ())
+ .toHashCode();
+ }
+
}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/KelpChunk.java b/core/src/main/java/de/pxav/kelp/core/world/KelpChunk.java
index 9a35c3db..59596ec6 100644
--- a/core/src/main/java/de/pxav/kelp/core/world/KelpChunk.java
+++ b/core/src/main/java/de/pxav/kelp/core/world/KelpChunk.java
@@ -4,11 +4,21 @@
import de.pxav.kelp.core.application.KelpApplication;
import de.pxav.kelp.core.player.KelpPlayer;
import de.pxav.kelp.core.world.version.ChunkVersionTemplate;
+import org.apache.commons.lang.builder.HashCodeBuilder;
import org.bukkit.Chunk;
import java.util.Collection;
import java.util.Set;
+/**
+ * A chunk is a 16x16 area of blocks used by Minecraft
+ * to generate the world and load a world dynamically.
+ *
+ * This class is a version-independent alternative to the
+ * normal {@link Chunk} class provided by bukkit.
+ *
+ * @author pxav
+ */
public class KelpChunk {
private Chunk bukkitChunk;
@@ -19,6 +29,12 @@ public class KelpChunk {
this.versionTemplate = versionTemplate;
}
+ /**
+ * Converts the bukkit chunk into a {@link KelpChunk}.
+ *
+ * @param bukkitChunk The bukkit chunk you want to convert.
+ * @return The final {@link KelpChunk}
+ */
public static KelpChunk from(Chunk bukkitChunk) {
return new KelpChunk(
bukkitChunk,
@@ -26,74 +42,186 @@ public static KelpChunk from(Chunk bukkitChunk) {
);
}
+ /**
+ * Gets the world the given chunk is located in.
+ *
+ * @return The world the given chunk is located in.
+ */
public KelpWorld getWorld() {
return versionTemplate.getWorld(this);
}
+ /**
+ * Gets the X-coordinate in the chunk's world of the given
+ * chunk. This can be used to compare and identify chunks.
+ *
+ * Note that this value does not return the absolute X-block-
+ * coordinate where the chunk begins, but it returns the X
+ * block value divided by 16. If you are at x=-35, then this
+ * would return 2, because it is bigger than 32 (16*2) but smaller
+ * than 48 (16*3).
+ *
+ * @return The X coordinate on the world's chunk grid.
+ */
public int getX() {
return versionTemplate.getX(this);
}
+ /**
+ * Gets the Z-coordinate in the chunk's world of the given
+ * chunk. This can be used to compare and identify chunks.
+ *
+ * Note that this value does not return the absolute Z-block-
+ * coordinate where the chunk begins, but it returns the Z
+ * block value divided by 16. If you are at z=-35, then this
+ * would return 2, because it is bigger than 32 (16*2) but smaller
+ * than 48 (16*3).
+ *
+ * @return The Z coordinate on the world's chunk grid.
+ */
public int getZ() {
return versionTemplate.getZ(this);
}
+ /**
+ * Gets a block at the given location inside the chunk. The passed location
+ * may not be from outside the chunk!
+ *
+ * @param location The location where the block is located.
+ * @return The block at the given location.
+ */
public KelpBlock getBlockAt(KelpLocation location) {
return versionTemplate.getBlockAt(this, location);
}
+ /**
+ * Determines whether the given location is inside the given chunk.
+ *
+ * @param location The location that should be located in the chunk.
+ * @return {@code true} if the location is inside the chunk.
+ */
public boolean contains(KelpLocation location) {
return this.versionTemplate.contains(this, location);
}
+ /**
+ * Determines whether the given block is inside the given chunk.
+ *
+ * @param block The block that should be located in the chunk.
+ * @return {@code true} if the block is inside the chunk.
+ */
public boolean contains(KelpBlock block) {
return this.versionTemplate.contains(this, block.getLocation());
}
+ /**
+ * Determines whether slime entities can spawn in the given chunk.
+ *
+ * @return {@code true} if slime entities can spawn in that chunk.
+ */
public boolean isSlimeChunk() {
return versionTemplate.isSlimeChunk(this);
}
+ /**
+ * Gets a collection of all players that are currently inside the chunk.
+ *
+ * @return A collection of all players that are currently inside the chunk.
+ */
public Collection getPlayers() {
return versionTemplate.getPlayers(this);
}
+ /**
+ * Gets the approximate center block of this chunk at the given height.
+ * As a chunk is 16x16 blocks, there is no exact center block, which
+ * is why this method can only return an approximation.
+ *
+ * @param height The height of the desired center block location.
+ * @return The approximate center of the given chunk.
+ */
public KelpLocation getCenter(int height) {
return KelpLocation.from(getWorld().getName(), getX() << 4, height, getZ() << 4).add(7, 0, 7);
}
+ /**
+ * Gets the chunk which is west of the current chunk.
+ *
+ * @return The chunk west of the current chunk.
+ */
public KelpChunk getWestEnclosedChunk() {
return getNorthWesternBlock(10).getLocation().add(-2, 0, 2).getChunk();
}
+ /**
+ * Gets the chunk which is north of the current chunk.
+ *
+ * @return The chunk north of the current chunk.
+ */
public KelpChunk getNorthEnclosedChunk() {
return getNorthEasternBlock(10).getLocation().add(-2, 0, -2).getChunk();
}
+ /**
+ * Gets the chunk which is north of the current chunk.
+ *
+ * @return The chunk north of the current chunk.
+ */
public KelpChunk getEastEnclosedChunk() {
return getNorthEasternBlock(10).getLocation().add(2, 0, 2).getChunk();
}
+ /**
+ * Gets the chunk which is south of the current chunk.
+ *
+ * @return The chunk south of the current chunk.
+ */
public KelpChunk getSouthEnclosedChunk() {
return getSouthWesternBlock(10).getLocation().add(2, 0, 2).getChunk();
}
+ /**
+ * Gets the chunk which is south west of the current chunk.
+ *
+ * @return The chunk south west of the current chunk.
+ */
public KelpChunk getSouthWestEnclosedChunk() {
return getSouthWesternBlock(10).getLocation().add(-2, 0, 2).getChunk();
}
+ /**
+ * Gets the chunk which is north west of the current chunk.
+ *
+ * @return The chunk north west of the current chunk.
+ */
public KelpChunk getNorthWestEnclosedChunk() {
return getNorthWesternBlock(10).getLocation().add(-2, 0, -2).getChunk();
}
+ /**
+ * Gets the chunk which is north east of the current chunk.
+ *
+ * @return The chunk north east of the current chunk.
+ */
public KelpChunk getNorthEastEnclosedChunk() {
return getNorthEasternBlock(10).getLocation().add(2, 0, -2).getChunk();
}
+ /**
+ * Gets the chunk which is south east of the current chunk.
+ *
+ * @return The chunk south east of the current chunk.
+ */
public KelpChunk getSouthEastEnclosedChunk() {
return getSouthEasternBlock(10).getLocation().add(2, 0, 2).getChunk();
}
+ /**
+ * Gets the block which is most north east in this chunk.
+ *
+ * @param height The height of the desired block.
+ * @return The most north eastern block of this chunk.
+ */
public KelpBlock getNorthEasternBlock(int height) {
KelpLocation location = KelpLocation.from(
getWorld().getName(),
@@ -104,6 +232,12 @@ public KelpBlock getNorthEasternBlock(int height) {
return getWorld().getBlockAt(location);
}
+ /**
+ * Gets the block which is most north west in this chunk.
+ *
+ * @param height The height of the desired block.
+ * @return The most north western block of this chunk.
+ */
public KelpBlock getNorthWesternBlock(int height) {
KelpLocation location = KelpLocation.from(
getWorld().getName(),
@@ -114,6 +248,12 @@ public KelpBlock getNorthWesternBlock(int height) {
return getWorld().getBlockAt(location);
}
+ /**
+ * Gets the block which is most south east in this chunk.
+ *
+ * @param height The height of the desired block.
+ * @return The most south eastern block of this chunk.
+ */
public KelpBlock getSouthEasternBlock(int height) {
KelpLocation location = KelpLocation.from(
getWorld().getName(),
@@ -124,6 +264,12 @@ public KelpBlock getSouthEasternBlock(int height) {
return getWorld().getBlockAt(location);
}
+ /**
+ * Gets the block which is most south west in this chunk.
+ *
+ * @param height The height of the desired block.
+ * @return The most south western block of this chunk.
+ */
public KelpBlock getSouthWesternBlock(int height) {
KelpLocation location = KelpLocation.from(
getWorld().getName(),
@@ -134,39 +280,113 @@ public KelpBlock getSouthWesternBlock(int height) {
return getWorld().getBlockAt(location);
}
+ /**
+ * Loads the chunk. This will make the chunk passable for players and
+ * tick operations such as redstone clocks or crop growing will be performed
+ * again. To save performance, chunks are unloaded by bukkit if they are not used.
+ * If you need to prevent that you can use {@link #addForceLoadFlag(Class)}
+ * to keep the chunk loaded until you manually unload it again. But please
+ * keep in mind that this might have bad performance impact.
+ */
public void load() {
versionTemplate.load(this);
}
+ /**
+ * Unloads the chunk. That means tick operations in this
+ * chunk will no longer be performed. Redstone clocks will stop running
+ * and crops will stop growing. This can be reversed at any time using
+ * {@link #load()}.
+ */
public void unload() {
versionTemplate.unload(this);
}
+ /**
+ * Checks whether the given chunk is currently loaded. That
+ * means it checks whether tick operations are currently ran on
+ * this chunk.
+ *
+ * @return {@code true} if the chunk is currently loaded.
+ */
public boolean isLoaded() {
return versionTemplate.isLoaded(this);
}
+ /**
+ * Adds a force load flag to the given chunk. A force load flag means that
+ * the chunk cannot be unloaded by bukkit randomly, but keeps loaded until you
+ * revert that action by yourself ({@link #removeForceLoadFlag(Class)}).
+ *
+ * If you call this method, {@link #unload()} won't have an effect
+ * as Kelp will immediately load the chunk again if there is a flag to keep
+ * it loaded. So if you want to unload the chunk, call {@link #removeForceLoadFlag(Class)}
+ * first.
+ *
+ * @param plugin The plugin that should keep the chunk loaded.
+ * This is important when you remove your flag,
+ * but another plugin still relies on the chunk to be loaded,
+ * the chunk will be kept loaded until the other plugin(s)
+ * unload the chunk as well.
+ */
public void addForceLoadFlag(Class extends KelpApplication> plugin) {
versionTemplate.addForceLoadFlag(this, plugin);
}
+ /**
+ * Removes a force load flag from the given chunk again. A force load flag means that
+ * the chunk cannot be unloaded by bukkit randomly, but keeps loaded until you
+ * revert that action by yourself using this method. Such a flag can be assigned
+ * to a chunk using {@link #addForceLoadFlag(Class)}.
+ *
+ * @param plugin The plugin that removes their flag. If another plugin has still
+ * loaded this chunk, it won't be unloaded until all plugins have removed
+ * their flag.
+ */
public void removeForceLoadFlag(Class extends KelpApplication> plugin) {
versionTemplate.removeForceLoadFlag(this, plugin);
}
+ /**
+ * Gets all {@link KelpApplication}s that are currently forcing this chunk to keep loaded.
+ *
+ * @return A set of all plugin main classes that force the chunk to keep loaded.
+ */
public Set> getForceLoadingPlugins() {
return versionTemplate.getForceLoadFlagPlugins(this);
}
- public boolean equals(KelpChunk compareTo) {
- return compareTo.getX() == getX() && compareTo.getZ() == getZ();
+ /**
+ * Gets the bukkit chunk instance of this KelpChunk.
+ *
+ * @return The bukkit instance of this chunk.
+ */
+ public Chunk getBukkitChunk() {
+ return bukkitChunk;
}
- public boolean equals(Chunk compareTo) {
- return compareTo.getX() == getX() && compareTo.getZ() == getZ();
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+
+ if (!(object instanceof KelpChunk)) {
+ return false;
+ }
+
+ KelpChunk chunk = (KelpChunk) object;
+ return chunk.getZ() == this.getZ()
+ && chunk.getX() == this.getX()
+ && chunk.getWorld().getName().equalsIgnoreCase(this.getWorld().getName());
}
- public Chunk getBukkitChunk() {
- return bukkitChunk;
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(this.getWorld().getName())
+ .append(this.getX())
+ .append(this.getZ())
+ .toHashCode();
}
}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/KelpLocation.java b/core/src/main/java/de/pxav/kelp/core/world/KelpLocation.java
index 47d0f749..fb7879f4 100644
--- a/core/src/main/java/de/pxav/kelp/core/world/KelpLocation.java
+++ b/core/src/main/java/de/pxav/kelp/core/world/KelpLocation.java
@@ -2,6 +2,7 @@
import com.google.common.base.Preconditions;
import de.pxav.kelp.core.world.util.CardinalDirection;
+import org.apache.commons.lang.builder.HashCodeBuilder;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
@@ -95,6 +96,10 @@ public static KelpLocation create() {
return new KelpLocation();
}
+ public static double magnitude(double x, double y, double z) {
+ return (x * x) + (y * y) + (z * z);
+ }
+
/**
* Gets the name of the world this location is valid for.
*
@@ -111,9 +116,11 @@ public String getWorldName() {
* when working with the location. This might cause lag to the server.
*
* @param worldName The name of the new world for this location.
+ * @return Instance of the current location
*/
- public void setWorldName(String worldName) {
+ public KelpLocation setWorldName(String worldName) {
this.worldName = worldName;
+ return this;
}
/**
@@ -129,9 +136,11 @@ public double getX() {
* Sets the location's exact value on the world's X-axis.
*
* @param x The new value on the world's X-axis for the location.
+ * @return Instance of the current location
*/
- public void setX(double x) {
+ public KelpLocation setX(double x) {
this.x = x;
+ return this;
}
/**
@@ -147,9 +156,11 @@ public double getY() {
* Sets the location's exact value on the world's Y-axis (height).
*
* @param y The new value on the world's Y-axis for the location.
+ * @return Instance of the current location
*/
- public void setY(double y) {
+ public KelpLocation setY(double y) {
this.y = y;
+ return this;
}
/**
@@ -165,9 +176,11 @@ public double getZ() {
* Sets the location's exact value on the world's Y-axis.
*
* @param z The new value on the world's Y-axis for the location.
+ * @return Instance of the current location
*/
- public void setZ(double z) {
+ public KelpLocation setZ(double z) {
this.z = z;
+ return this;
}
/**
@@ -194,9 +207,11 @@ public float getYaw() {
* Sets the location's yaw (rotation around the y-Axis).
*
* @param yaw The new yaw to set for this location.
+ * @return Instance of the current location
*/
- public void setYaw(float yaw) {
+ public KelpLocation setYaw(float yaw) {
this.yaw = yaw;
+ return this;
}
/**
@@ -218,9 +233,11 @@ public float getPitch() {
* Sets the locations pitch. This is the angle in which the entity is facing up or down.
*
* @param pitch The new pitch for the location.
+ * @return Instance of the current location
*/
- public void setPitch(float pitch) {
+ public KelpLocation setPitch(float pitch) {
this.pitch = pitch;
+ return this;
}
/**
@@ -263,9 +280,11 @@ public int getBlockZ() {
* so be careful if you still need the exact value.
*
* @param x The new block x-value of this location.
+ * @return Instance of the current location
*/
- public void setBlockX(double x) {
+ public KelpLocation setBlockX(double x) {
this.x = Location.locToBlock(this.x);
+ return this;
}
/**
@@ -275,9 +294,11 @@ public void setBlockX(double x) {
* so be careful if you still need the exact value.
*
* @param y The new block y-value of this location.
+ * @return Instance of the current location
*/
- public void setBlockY(double y) {
+ public KelpLocation setBlockY(double y) {
this.y = Location.locToBlock(this.y);
+ return this;
}
/**
@@ -287,9 +308,11 @@ public void setBlockY(double y) {
* so be careful if you still need the exact value.
*
* @param z The new block z-value of this location.
+ * @return Instance of the current location
*/
- public void setBlockZ(double z) {
+ public KelpLocation setBlockZ(double z) {
this.z = Location.locToBlock(this.z);
+ return this;
}
/**
@@ -329,6 +352,21 @@ public KelpLocation add(double x, double y, double z) {
return this;
}
+ /**
+ * Adds the given value to all coordinate values of this location.
+ * This means that all axis {@code x, y and z} will grow
+ * by {@code value}.
+ *
+ * @param value The value to add to all location coordinates.
+ * @return The current location object with the added values.
+ */
+ public KelpLocation add(double value) {
+ this.x += value;
+ this.y += value;
+ this.z += value;
+ return this;
+ }
+
/**
* Only adds the x-coordinate of this location.
* Other coordinates are not affected by this method.
@@ -396,6 +434,20 @@ public KelpLocation subtract(double x, double y, double z) {
return this;
}
+ /**
+ * Subtracts the given value from all the locations coordinates.
+ * So all axis {@code x, y and z} will be smaller by {@code value}.
+ *
+ * @param value The value to subtract from all coordinate values.
+ * @return The current location object with the subtracted values.
+ */
+ public KelpLocation subtract(double value) {
+ this.x -= value;
+ this.y -= value;
+ this.z -= value;
+ return this;
+ }
+
/**
* Only subtracts the x-coordinate of this location.
* Other values are not affected by this method.
@@ -604,6 +656,10 @@ public CardinalDirection getCardinalDirection() {
}
}
+ public KelpLocation findMidpoint(KelpLocation to) {
+ return getMinimalLocation(to).add(getMaximalLocation(to)).multiply(0.5);
+ }
+
/**
* Sets the yaw and pitch value of this location based on any
* vector. The length of the vector is ignored for this operation.
@@ -835,6 +891,53 @@ public KelpLocation multiply(KelpLocation multiplier) {
return this;
}
+ /**
+ * Compares the current location with the given location
+ * and returns the location that is lower in the world's grid.
+ * When a location is 'lower' than another it means that its
+ * coordinate values (x, y, z) are smaller. The yaw and pitch
+ * value is ignored in this calculation.
+ *
+ * This method will automatically clone the source location.
+ *
+ * @param compareTo The location to compare the current location to.
+ * @return The location that is lower in the world grid.
+ */
+ public KelpLocation getMinimalLocation(KelpLocation compareTo) {
+ double minX = Math.min(compareTo.getX(), getX());
+ double minY = Math.min(compareTo.getY(), getY());
+ double minZ = Math.min(compareTo.getZ(), getZ());
+
+ return this.clone()
+ .setX(minX)
+ .setY(minY)
+ .setZ(minZ);
+ }
+
+
+ /**
+ * Compares the current location with the given location
+ * and returns the location that is higher in the world's grid.
+ * When a location is 'higher' than another it means that its
+ * coordinate values (x, y, z) are bigger. The yaw and pitch
+ * value is ignored in this calculation.
+ *
+ * This method will automatically clone the source location.
+ *
+ * @param compareTo The location to compare the current location to.
+ * @return The location that is higher in the world grid.
+ */
+ public KelpLocation getMaximalLocation(KelpLocation compareTo) {
+ double maxX = Math.max(compareTo.getX(), getX());
+ double maxY = Math.max(compareTo.getY(), getY());
+ double maxZ = Math.max(compareTo.getZ(), getZ());
+
+ return this.clone()
+ .setX(maxX)
+ .setY(maxY)
+ .setZ(maxZ);
+ }
+
/**
* Zeros all axis of the location. This sets the x, y, and z
* coordinate to {@code 0}.
@@ -896,4 +999,35 @@ public Location getBukkitLocation() {
return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
}
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+
+ if (!(object instanceof KelpLocation)) {
+ return false;
+ }
+
+ KelpLocation location = (KelpLocation) object;
+ return this.getWorldName().equalsIgnoreCase(location.getWorldName())
+ && this.getX() == location.getX()
+ && this.getY() == location.getY()
+ && this.getZ() == location.getZ()
+ && this.getYaw() == location.getYaw()
+ && this.getPitch() == location.getPitch();
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(this.worldName)
+ .append(this.x)
+ .append(this.y)
+ .append(this.z)
+ .append(this.yaw)
+ .append(this.pitch)
+ .toHashCode();
+ }
+
}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/KelpWorld.java b/core/src/main/java/de/pxav/kelp/core/world/KelpWorld.java
index 5d9b4870..2925f564 100644
--- a/core/src/main/java/de/pxav/kelp/core/world/KelpWorld.java
+++ b/core/src/main/java/de/pxav/kelp/core/world/KelpWorld.java
@@ -11,6 +11,7 @@
import org.bukkit.Bukkit;
import org.bukkit.Difficulty;
import org.bukkit.World;
+import org.bukkit.craftbukkit.v1_16_R3.CraftWorld;
import java.util.Collection;
import java.util.UUID;
@@ -87,6 +88,20 @@ public KelpBlock getBlockAt(KelpLocation location) {
return versionTemplate.getBlockAt(this, location);
}
+ /**
+ * Gets the {@link KelpBlock} at the given location of the world.
+ *
+ * @param x The x-coordinate of the location you want to get the block at.
+ * @param y The y-coordinate of the location you want to get the block at.
+ * @param z The z-coordinate of the location you want to get the block at.
+ * @return The {@link KelpBlock} object at the given location.
+ * If the block's material is {@link de.pxav.kelp.core.inventory.material.KelpMaterial#AIR},
+ * the block won't be {@code null} but of type {@code AIR}
+ */
+ public KelpBlock getBlockAt(double x, double y, double z) {
+ return versionTemplate.getBlockAt(this, KelpLocation.from(getName(), x, y, z));
+ }
+
/**
* Gets the chunk at the given location in this world.
* A chunk is a 16x16x256 (320 if you are on 1.17+)
@@ -100,6 +115,19 @@ public KelpChunk getChunkAt(KelpLocation location) {
return versionTemplate.getChunkAt(this, location);
}
+ /**
+ * Gets the chunk at the given location in this world.
+ * More information can be found in {@link KelpChunk}.
+ *
+ * @param x The x-coordinate of the location to get the chunk at.
+ * @param y The y-coordinate of the location to get the chunk at.
+ * @param z The z-coordinate of the location to get the chunk at.
+ * @return The chunk at the given location.
+ */
+ public KelpChunk getChunkAt(double x, double y, double z) {
+ return versionTemplate.getChunkAt(this, KelpLocation.from(getName(), x, y, z));
+ }
+
/**
* Gets the name of this world in the bukkit world registration.
*
@@ -613,4 +641,23 @@ public KelpWorld strikeLightningEffect(KelpLocation location) {
return this;
}
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+
+ if (!(object instanceof KelpWorld)) {
+ return false;
+ }
+
+ KelpWorld world = (KelpWorld) object;
+ return world.getName().equalsIgnoreCase(this.getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return getUUID().hashCode();
+ }
+
}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/region/CuboidRegion.java b/core/src/main/java/de/pxav/kelp/core/world/region/CuboidRegion.java
new file mode 100644
index 00000000..5c0e41ba
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/world/region/CuboidRegion.java
@@ -0,0 +1,442 @@
+package de.pxav.kelp.core.world.region;
+
+import com.google.common.collect.Sets;
+import de.pxav.kelp.core.world.KelpBlock;
+import de.pxav.kelp.core.world.KelpChunk;
+import de.pxav.kelp.core.world.KelpLocation;
+import de.pxav.kelp.core.world.util.KelpBlockFace;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.bukkit.util.Vector;
+
+import java.util.Set;
+
+/**
+ * A CuboidRegion is a region representing a cubic space in a 3d-world.
+ * This means it has two opposite points and calculates the cuboid in between
+ * those points.
+ *
+ * @author pxav
+ */
+public class CuboidRegion extends KelpRegion {
+
+ /**
+ * Creates a new {@link CuboidRegion} based on two opposite points.
+ * This method automatically calculates which point is the upper and
+ * which one the lower point.
+ *
+ * @param pos1 The first point
+ * @param pos2 The second point.
+ * @return The cuboid region between those points
+ */
+ public static CuboidRegion create(KelpLocation pos1, KelpLocation pos2) {
+ CuboidRegion region = new CuboidRegion(getRegionRepository());
+ region.setBoundingPositions(pos1, pos2);
+ region.setWorldName(pos1.getWorldName());
+ return region;
+ }
+
+ /**
+ * Creates a new {@link CuboidRegion} instance without calculating any points.
+ *
+ * @return The fresh and empty cuboid region instance.
+ */
+ public static CuboidRegion create() {
+ return new CuboidRegion(getRegionRepository());
+ }
+
+ private CuboidRegion(KelpRegionRepository regionRepository) {
+ super(regionRepository);
+ }
+
+ /**
+ * Moves the region into a certain direction defined by
+ * the different axis values.
+ *
+ * @param dx How far the region should be moved on the x-axis
+ * @param dy How far the region should be moved on the y-axis
+ * @param dz How far the region should be moved on the z-axis
+ */
+ @Override
+ protected void moveIgnoreListeners(double dx, double dy, double dz) {
+ move(new Vector(dx, dy, dz));
+ }
+
+ /**
+ * Moves the region into the direction of the given
+ * {@link Vector}.
+ *
+ * @param vector The vector providing the direction and
+ * power of the movement.
+ */
+ @Override
+ protected void moveIgnoreListeners(Vector vector) {
+ this.minPos.add(vector);
+ this.maxPos.add(vector);
+ }
+
+ /**
+ * Gets the total volume of this region, which is defined
+ * by the length on the {@code x * y * z} axis.
+ *
+ * This method returns the exact volume of this region.
+ * If you only want the block count, use {@link #getBlockVolume()}
+ * instead.
+ *
+ * @return The exact volume of this location in blocks.
+ */
+ @Override
+ public double getVolume() {
+ double[] dimensions = getDimensions();
+ return dimensions[0] * dimensions[1] * dimensions[2];
+ }
+
+ /**
+ * Gets the total volume of this region, which is defined
+ * by the length on the {@code x * y * z} axis.
+ *
+ * This method approximates the volume to whole blocks.
+ *
+ * @return The approximate volume of this location in blocks.
+ */
+ @Override
+ public int getBlockVolume() {
+ int[] blockDimensions = getBlockDimensions();
+ return blockDimensions[0] * blockDimensions[1] * blockDimensions[2];
+ }
+
+ /**
+ * Determines whether the given location is contained by
+ * this region. This method does not check whether the world
+ * is equal but only the axis values.
+ *
+ * @param x The x-coordinate of the location to check.
+ * @param y The y-coordinate of the location to check.
+ * @param z The z-coordinate of the location to check.
+ * @return {@code true} if the location is contained by the region.
+ */
+ @Override
+ public boolean contains(double x, double y, double z) {
+ // X
+ double maxX = Math.max(this.minPos.getX(), this.maxPos.getX());
+ double minX = Math.min(this.minPos.getX(), this.maxPos.getX());
+
+ // Y
+ double maxY = Math.max(this.minPos.getY(), this.maxPos.getY());
+ double minY = Math.min(this.minPos.getY(), this.maxPos.getY());
+
+ // Z
+ double maxZ = Math.max(this.minPos.getZ(), this.maxPos.getZ());
+ double minZ = Math.min(this.minPos.getZ(), this.maxPos.getZ());
+
+ if(x <= maxX && x >= minX) {
+ if(y <= maxY && y >= minY) {
+ return z <= maxZ && z >= minZ;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the center of the cuboid, which is defined by the center of the
+ * diagonal line between the two opposite points.
+ *
+ * @return The center of this cuboid.
+ */
+ @Override
+ public KelpLocation getCenter() {
+ return this.minPos.clone().add(this.maxPos).multiply(0.5);
+ }
+
+ /**
+ * Checks if this region intersects with another {@link CuboidRegion}.
+ *
+ * @param region The region to check the intersection with.
+ * @return {@code true} whether both regions intersect with each other.
+ */
+ public boolean hasCuboidIntersection(CuboidRegion region) {
+ // if the worlds differ, the regions cannot intersect
+ if (!region.getWorldName().equalsIgnoreCase(worldName)) {
+ return false;
+ }
+
+ return (!(minPos.getX() > region.getMaxPos().getX() || region.getMinPos().getX() > maxPos.getX()
+ || minPos.getY() > region.getMaxPos().getY() || region.getMinPos().getY() > maxPos.getY()
+ || minPos.getZ() > region.getMaxPos().getZ() || region.getMinPos().getZ() > maxPos.getZ()));
+ }
+
+ /**
+ * Gets the cuboid intersection between to regions. If two
+ * {@link CuboidRegion}s intersect with each other, the
+ * intersecting blocks will be covered by this region.
+ *
+ * @param region The region you want to get the intersection with.
+ * @return The region representing the intersection of the two regions.
+ */
+ public CuboidRegion cuboidIntersection(CuboidRegion region) {
+ // if the regions do not intersect, return null
+ if (!hasCuboidIntersection(region)) {
+ return null;
+ }
+
+ return CuboidRegion.create(
+ getMinPos().getMaximalLocation(region.getMinPos()),
+ getMaxPos().getMinimalLocation(region.getMaxPos()));
+ }
+
+ /**
+ * Gets a set of all blocks which are on the regions surface.
+ *
+ * @return A set of blocks containing all blocks on the regions surface.
+ */
+ @Override
+ public Set getSurfaceBlocks() {
+ Set output = Sets.newConcurrentHashSet();
+ output.addAll(getFaceBlocks(KelpBlockFace.UP));
+ output.addAll(getFaceBlocks(KelpBlockFace.DOWN));
+ output.addAll(getFaceBlocks(KelpBlockFace.EAST));
+ output.addAll(getFaceBlocks(KelpBlockFace.WEST));
+ output.addAll(getFaceBlocks(KelpBlockFace.NORTH));
+ output.addAll(getFaceBlocks(KelpBlockFace.SOUTH));
+ return output;
+ }
+
+ /**
+ * Gets all blocks contained by this region.
+ * This can be used to visualize its shape.
+ *
+ * @return A set of all blocks contained by this region.
+ */
+ @Override
+ public Set getBlocks() {
+ Set output = Sets.newConcurrentHashSet();
+ for (int y = minPos.getBlockY(); y <= maxPos.getBlockY(); y++) {
+ for (int x = minPos.getBlockX(); x <= maxPos.getBlockX(); x++) {
+ for (int z = minPos.getBlockZ(); z <= maxPos.getBlockZ(); z++) {
+ output.add(KelpLocation.from(getWorldName(), x, y, z).getBlock());
+ }
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Gets all blocks of a specific face of this cuboid.
+ *
+ * @param direction The direction of the face you want to get the blocks of.
+ * @return A set of blocks of the face in the given direction.
+ */
+ public Set getFaceBlocks(KelpBlockFace direction) {
+ return getFace(direction).getBlocks();
+ }
+
+ /**
+ * Gets the length of the region in the given direction.
+ * This method gets the exact length, use {@link #measureBlocks(KelpBlockFace)}
+ * if you only need the block count instead.
+ *
+ * @param direction The direction to measure the length of.
+ * @return The length of this cuboid in a specific direction.
+ */
+ public double measure(KelpBlockFace direction) {
+ switch (direction) {
+ case UP:
+ case DOWN:
+ return getDimensions()[1];
+ case EAST:
+ case WEST:
+ return getDimensions()[0];
+ case NORTH:
+ case SOUTH:
+ return getDimensions()[2];
+ }
+ throw new IllegalArgumentException("Cannot measure region axis '" + direction + "'. Only use UP, DOWN, EAST, WEST, NORTH, SOUTH");
+ }
+
+ /**
+ * Gets the length of the region in the given direction.
+ * This method approximates the output to an integer
+ * block count.
+ *
+ * @param direction The direction to measure the length of.
+ * @return The length of this cuboid in a specific direction.
+ */
+ public int measureBlocks(KelpBlockFace direction) {
+ switch (direction) {
+ case UP:
+ case DOWN:
+ return getBlockDimensions()[1];
+ case EAST:
+ case WEST:
+ return getBlockDimensions()[0];
+ case NORTH:
+ case SOUTH:
+ return getBlockDimensions()[2];
+ }
+ throw new IllegalArgumentException("Cannot measure region axis '" + direction + "'. Only use UP, DOWN, EAST, WEST, NORTH, SOUTH");
+ }
+
+ /**
+ * Gets all chunks covered by this region no matter if they
+ * are loaded or not. If you only want loaded chunks,
+ * use {@link #getLoadedChunks()} instead.
+ *
+ * @return A set of all chunks covered by this region.
+ */
+ @Override
+ public Set getChunks() {
+ Set output = Sets.newHashSet();
+ KelpChunk minChunk = getMinPos().getChunk();
+ KelpChunk maxChunk = getMaxPos().getChunk();
+
+ for (int cx = minChunk.getX(); cx <= maxChunk.getX(); cx++) {
+ for (int cz = minChunk.getZ(); cz <= maxChunk.getZ(); cz++) {
+ output.add(getWorld().getChunkAt(cx, 0, cz));
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Gets all chunks that are loaded and covered by this region.
+ * If you want to include unloaded chunks as well, use {@link #getChunks()}
+ * instead.
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @return A set of all loaded chunks covered by this region.
+ */
+ @Override
+ public Set getLoadedChunks() {
+ Set output = Sets.newHashSet();
+ KelpChunk minChunk = getMinPos().getChunk();
+ KelpChunk maxChunk = getMaxPos().getChunk();
+
+ for (int cx = minChunk.getX(); cx <= maxChunk.getX(); cx++) {
+ for (int cz = minChunk.getZ(); cz <= maxChunk.getZ(); cz++) {
+ KelpChunk toAdd = getWorld().getChunkAt(cx, 0, cz);
+ if (toAdd.isLoaded()) {
+ output.add(toAdd);
+ }
+ }
+ }
+ return output;
+ }
+
+ /**
+ * Gets the cuboid region representing the outermost block-layer
+ * at a given face. If you choose {@link KelpBlockFace} for example,
+ *
+ * @param direction The direction of the face you want to get.
+ * @return The cuboid region representing the face in the given direction.
+ */
+ public CuboidRegion getFace(KelpBlockFace direction) {
+ CuboidRegion output = this.clone();
+ output.expand(direction.getOppositeFace(), -output.measureBlocks(direction) + 1);
+ return output;
+ }
+
+ /**
+ * Expands the current region by a certain multiplier.
+ * This method does not update the listeners for the region.
+ *
+ * @param amount The amount of expansion in blocks.
+ */
+ @Override
+ protected void expandIgnoreListeners(double amount) {
+ expand(amount, amount, amount, amount, amount, amount);
+ }
+
+ /**
+ * Expands the current region into a specific direction by
+ * a given multiplier.
+ * This method does not update the listeners for the region.
+ *
+ * @param direction The direction to expand the region in.
+ * @param amount The amount of expansion in blocks.
+ */
+ @Override
+ protected void expandIgnoreListeners(KelpBlockFace direction, double amount) {
+ Vector vector = direction.getDirection();
+ if (vector.getX() + vector.getY() + vector.getZ() > 0) {
+ vector = vector.multiply(amount);
+ maxPos.add(vector);
+ } else {
+ vector = vector.multiply(amount);
+ minPos.add(vector);
+ }
+ setBoundingPositions(minPos, maxPos);
+ }
+
+ /**
+ * Expands the current region in the given axis.
+ * This method does not update the listeners for the region.
+ *
+ * @param negativeX The expansion on the negative x-axis (equal to west)
+ * @param positiveX The expansion on the positive x-axis (equal to east)
+ * @param negativeY The expansion on the negative y-axis (equal to down)
+ * @param positiveY The expansion on the positive y-axis (equal to up)
+ * @param negativeZ The expansion on the negative z-axis (equal to north)
+ * @param positiveZ The expansion on the positive z-axis (equal to south)
+ */
+ @Override
+ protected void expandIgnoreListener(double negativeX, double positiveX, double negativeY, double positiveY, double negativeZ, double positiveZ) {
+ this.minPos.subtract(negativeX, negativeY, negativeZ);
+ this.maxPos.add(positiveX, positiveY, positiveZ);
+ setBoundingPositions(minPos, maxPos);
+ }
+
+ @Override
+ public CuboidRegion clone() {
+ return CuboidRegion.create(this.minPos, this.maxPos);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof CuboidRegion)) {
+ return false;
+ }
+
+ CuboidRegion region = (CuboidRegion) object;
+ return region.getWorldName().equalsIgnoreCase(worldName)
+ && minPos.equals(region.getMinPos())
+ && maxPos.equals(region.getMaxPos());
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(this.worldName)
+ .append("CUBOID")
+ .append(maxPos.hashCode())
+ .append(minPos.hashCode())
+ .toHashCode();
+ }
+
+ /**
+ * Takes two opposite points and checks which of those points
+ * is higher and which one is lower and assigns them to {@code minPos}
+ * and {@code maxPos} of this location accordingly.
+ *
+ * The order you provide the points in does not matter for this function.
+ *
+ * @param pos1 The first point
+ * @param pos2 The second point
+ */
+ public void setBoundingPositions(KelpLocation pos1, KelpLocation pos2) {
+ if (!pos1.getWorldName().equals(pos2.getWorldName())) {
+ throw new IllegalArgumentException("Cannot build CuboidRegion from locations of differing worlds!");
+ }
+
+ double minX = Math.min(pos1.getX(), pos2.getX());
+ double minY = Math.min(pos1.getY(), pos2.getY());
+ double minZ = Math.min(pos1.getZ(), pos2.getZ());
+
+ double maxX = Math.max(pos1.getX(), pos2.getX());
+ double maxY = Math.max(pos1.getY(), pos2.getY());
+ double maxZ = Math.max(pos1.getZ(), pos2.getZ());
+
+ this.minPos = KelpLocation.from(pos1.getWorldName(), minX, minY, minZ);
+ this.maxPos = KelpLocation.from(pos1.getWorldName(), maxX, maxY, maxZ);
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/region/EllipsoidRegion.java b/core/src/main/java/de/pxav/kelp/core/world/region/EllipsoidRegion.java
new file mode 100644
index 00000000..c19b5237
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/world/region/EllipsoidRegion.java
@@ -0,0 +1,779 @@
+package de.pxav.kelp.core.world.region;
+
+import com.google.common.collect.Sets;
+import de.pxav.kelp.core.world.KelpBlock;
+import de.pxav.kelp.core.world.KelpChunk;
+import de.pxav.kelp.core.world.KelpLocation;
+import de.pxav.kelp.core.world.util.KelpBlockFace;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.bukkit.Location;
+import org.bukkit.util.NumberConversions;
+import org.bukkit.util.Vector;
+
+import java.util.Set;
+
+/**
+ * An {@code EllipsoidRegion} represents an ellipsoid shape in a 3d-world.
+ * An ellipsoid is the 3d-equivalent of an ellipse and has different cases:
+ * - a sphere: all radius (xRadius, yRadius and zRadius) are equal to each
+ * other and every outer point of the shape has an equal distance to the center.
+ * - a spheroid: an ellipse has been rotated around itself. At least two
+ * radius values have to be equal to each other.
+ * - a triaxial ellipsoid: all radius values are different from each other
+ * and there is no symmetry. (most general)
+ *
+ * You can check the special cases using {@link #isSphere()} or
+ * {@link #isSpheroid()}.
+ *
+ * An ellipsoid can have a limited radius, which means that it has an
+ * ellipsoid shape, but is interrupted at some point. Then it has a
+ * straight border at the given axis as if you would cut it with a 1000° hot
+ * knife. You can enable those limiters for specific axis using
+ * {@link #limitXRadius(double)} for example.
+ *
+ * @author pxav
+ */
+public class EllipsoidRegion extends KelpRegion {
+
+ // four thirds are needed to calculate the volume of an ellipsoid,
+ // so it is cached here in order to save performance.
+ private static final double FOUR_THIRDS = 1.33333333333333333333d;
+
+ // the center of the ellipsoid. If the ellipsoid is a sphere,
+ // all surface locations will have the same distance to this block.
+ private KelpLocation center;
+
+ // radius in different directions
+ private double xRadius;
+ private double yRadius;
+ private double zRadius;
+
+ // the radius limiters
+ private double limitX = 0;
+ private double limitY = 0;
+ private double limitZ = 0;
+
+ /**
+ * Creates a new {@link EllipsoidRegion} with a given center and
+ * a radius in all directions. So this ellipsoid will be a sphere.
+ *
+ * @param center The center of the sphere.
+ * @param radius The sphere's radius.
+ * @return The sphere as an {@link EllipsoidRegion}
+ */
+ public static EllipsoidRegion create(KelpLocation center, double radius) {
+ EllipsoidRegion region = new EllipsoidRegion(getRegionRepository());
+ region.setCenter(center);
+ region.setRadius(radius);
+ region.minPos = center.clone().subtract(radius);
+ region.maxPos = center.clone().add(radius);
+ region.worldName = center.getWorldName();
+ return region;
+ }
+
+ /**
+ * Creates a new {@link EllipsoidRegion} with a given center and
+ * different radius values in each direction. So this ellipsoid will
+ * either be a sphreid or triaxial.
+ *
+ * @param center The center of your ellipsoid.
+ * @param xRadius The radius on the x axis.
+ * @param yRadius The radius on the x axis.
+ * @param zRadius The radius on the x axis.
+ * @return The final {@link EllipsoidRegion}
+ */
+ public static EllipsoidRegion create(KelpLocation center, double xRadius, double yRadius, double zRadius) {
+ EllipsoidRegion region = new EllipsoidRegion(getRegionRepository());
+ region.setCenter(center);
+ region.setXRadius(xRadius);
+ region.setYRadius(yRadius);
+ region.setZRadius(zRadius);
+ region.minPos = center.clone().subtract(xRadius, yRadius, zRadius);
+ region.maxPos = center.clone().add(xRadius, yRadius, zRadius);
+ region.worldName = center.getWorldName();
+ return region;
+ }
+
+ /**
+ * Creates a new {@link EllipsoidRegion} which fits into the cuboid
+ * area defined by the two given points. So the outermost points
+ * will touch the same axis as the limiter blocks, while the sphere
+ * wont exceed its area.
+ *
+ * @param pos1 The first point
+ * @param pos2 The second point.
+ * @return The ellipsoid fitting into the cuboid defined by pos1 and pos2.
+ */
+ public static EllipsoidRegion create(KelpLocation pos1, KelpLocation pos2) {
+ if (!pos1.getWorldName().equalsIgnoreCase(pos2.getWorldName())) {
+ throw new IllegalArgumentException("Cannot create region from locations of differing worlds!");
+ }
+ EllipsoidRegion region = new EllipsoidRegion(getRegionRepository());
+ region.maxPos = pos1.getMaximalLocation(pos2);
+ region.minPos = pos1.getMinimalLocation(pos2);
+
+ region.setCenter(pos1.findMidpoint(pos2));
+ region.worldName = pos1.getWorldName();
+
+ double radius = Math.abs(region.minPos.getX() - region.maxPos.getX()) * 0.5;
+ region.setXRadius(radius);
+ region.setYRadius(radius);
+ region.setZRadius(radius);
+
+ return region;
+ }
+
+ private EllipsoidRegion(KelpRegionRepository regionRepository) {
+ super(regionRepository);
+ }
+
+ /**
+ * Limits the ellipsoid radius in the all axis.
+ *
+ * Limiting the radius means that if the radius is set to
+ * 10, while the limiter is 5, the ellipsoid will be calculated
+ * as a normal 10-radius-ellipsoid, but it will be cut off straight
+ * as soon as the limiter is exceeded. This will look
+ * like someone cut with a knife through the ellipsoid.
+ *
+ * @param limiter The value to limit all radius values to.
+ */
+ public void limitRadius(double limiter) {
+ this.limitX = limiter;
+ this.limitY = limiter;
+ this.limitZ = limiter;
+ }
+
+ /**
+ * Limits the ellipsoid radius in the x axis.
+ *
+ * Limiting the radius means that if the radius is set to
+ * 10, while the limiter is 5, the ellipsoid will be calculated
+ * as a normal 10-radius-ellipsoid, but it will be cut off straight
+ * as soon as the limiter is exceeded. This will look
+ * like someone cut with a knife through the ellipsoid.
+ *
+ * @param limiter The value to limit all radius values to.
+ */
+ public void limitXRadius(double limiter) {
+ this.limitX = limiter;
+ }
+
+ /**
+ * Limits the ellipsoid radius in the x axis.
+ *
+ * Limiting the radius means that if the radius is set to
+ * 10, while the limiter is 5, the ellipsoid will be calculated
+ * as a normal 10-radius-ellipsoid, but it will be cut off straight
+ * as soon as the limiter is exceeded. This will look
+ * like someone cut with a knife through the ellipsoid.
+ *
+ * @param limiter The value to limit all radius values to.
+ */
+ public void limitYRadius(double limiter) {
+ this.limitY = limiter;
+ }
+
+ /**
+ * Limits the ellipsoid radius in the x axis.
+ *
+ * Limiting the radius means that if the radius is set to
+ * 10, while the limiter is 5, the ellipsoid will be calculated
+ * as a normal 10-radius-ellipsoid, but it will be cut off straight
+ * as soon as the limiter is exceeded. This will look
+ * like someone cut with a knife through the ellipsoid.
+ *
+ * @param limiter The value to limit all radius values to.
+ */
+ public void limitZRadius(double limiter) {
+ this.limitZ = limiter;
+ }
+
+ /**
+ * Moves the region into the direction of the given
+ * {@link Vector}.
+ *
+ * @param vector The vector providing the direction and
+ * power of the movement.
+ */
+ @Override
+ protected void moveIgnoreListeners(Vector vector) {
+ center.add(vector);
+ minPos.add(vector);
+ maxPos.add(vector);
+ }
+
+ /**
+ * Moves the region into a certain direction defined by
+ * the different axis values.
+ *
+ * @param dx How far the region should be moved on the x-axis
+ * @param dy How far the region should be moved on the y-axis
+ * @param dz How far the region should be moved on the z-axis
+ */
+ @Override
+ protected void moveIgnoreListeners(double dx, double dy, double dz) {
+ Vector vector = new Vector(dx, dy, dz);
+ center.add(vector);
+ minPos.add(vector);
+ maxPos.add(vector);
+ }
+
+ /**
+ * Gets the exact total volume of this region, which is defined
+ * by {@code (4/3) * PI * xRadius * yRadius * zRadius}
+ * for all ellipsoid.
+ *
+ * @return The exact volume of this region in blocks.
+ */
+ @Override
+ public double getVolume() {
+ return FOUR_THIRDS * Math.PI * xRadius * yRadius * zRadius;
+ }
+
+ /**
+ * Gets the approximate total volume of this region, which is defined
+ * by {@code (4/3) * PI * xRadius * yRadius * zRadius}
+ * for all ellipsoid.
+ *
+ * This method approximates the volume in whole blocks.
+ *
+ * @return The approximate volume of this region in blocks.
+ */
+ @Override
+ public int getBlockVolume() {
+ return NumberConversions.floor(getVolume());
+ }
+
+ /**
+ * Sets the center location of this ellipsoid.
+ *
+ * @param center The new center.
+ * @return An instance of this region for fluent builder design.
+ */
+ public EllipsoidRegion setCenter(KelpLocation center) {
+ this.center = center;
+ return this;
+ }
+
+ /**
+ * Gets the center of this ellipsoid.
+ *
+ * @return The center point of this ellipsoid.
+ */
+ @Override
+ public KelpLocation getCenter() {
+ return this.center;
+ }
+
+ /**
+ * Gets a set of all blocks which are on the regions surface.
+ *
+ * @return A set of blocks containing all blocks on the regions surface.
+ */
+ @Override
+ public Set getSurfaceBlocks() {
+ return getBlocks(true);
+ }
+
+ /**
+ * Gets all blocks contained by this region.
+ * This can be used to visualize its shape.
+ *
+ * @return A set of all blocks contained by this region.
+ */
+ @Override
+ public Set getBlocks() {
+ return getBlocks(false);
+ }
+
+ /**
+ * Gets all blocks contained by this region and optionally
+ * only the blocks contained by the surface. This method
+ * obeys all criteria set by the radius limiter.
+ *
+ * @param surfaceOnly Whether only surface blocks should be returned.
+ * @return All (surface) blocks of this region.
+ */
+ private Set getBlocks(boolean surfaceOnly) {
+ Set output = Sets.newConcurrentHashSet();
+ double rX = xRadius + 0.5;
+ double rY = yRadius + 0.5;
+ double rZ = zRadius + 0.5;
+
+ final double invRadiusX = 1 / rX;
+ final double invRadiusY = 1 / rY;
+ final double invRadiusZ = 1 / rZ;
+
+ final int ceilRadiusX = (int) Math.ceil(rX);
+ final int ceilRadiusY = (int) Math.ceil(rY);
+ final int ceilRadiusZ = (int) Math.ceil(rZ);
+
+ double nextXn = 0;
+ forX: for (int x = 0; x <= ceilRadiusX; ++x) {
+ final double xn = nextXn;
+ nextXn = (x + 1) * invRadiusX;
+ double nextYn = 0;
+ forY: for (int y = 0; y <= ceilRadiusY; ++y) {
+ final double yn = nextYn;
+ nextYn = (y + 1) * invRadiusY;
+ double nextZn = 0;
+ forZ: for (int z = 0; z <= ceilRadiusZ; ++z) {
+ final double zn = nextZn;
+ nextZn = (z + 1) * invRadiusZ;
+
+ double magnitude = KelpLocation.magnitude(xn, yn, zn);
+ if (magnitude > 1) {
+ if (z == 0) {
+ if (y == 0) {
+ break forX;
+ }
+ break forY;
+ }
+ break forZ;
+ }
+
+ if (surfaceOnly) {
+ if (KelpLocation.magnitude(nextXn, yn, zn) <= 1
+ && KelpLocation.magnitude(xn, nextYn, zn) <= 1
+ && KelpLocation.magnitude(xn, yn, nextZn) <= 1) {
+ continue;
+ }
+ }
+
+ output.add(center.clone().add(x, y, z).getBlock());
+ output.add(center.clone().add(-x, y, z).getBlock());
+ output.add(center.clone().add(x, -y, z).getBlock());
+ output.add(center.clone().add(x, y, -z).getBlock());
+ output.add(center.clone().add(-x, -y, z).getBlock());
+ output.add(center.clone().add(x, -y, -z).getBlock());
+ output.add(center.clone().add(-x, y, -z).getBlock());
+ output.add(center.clone().add(-x, -y, -z).getBlock());
+ }
+ }
+ }
+
+ // if limiter is enabled, remove all blocks out of the allowed radius
+ if (limitX > 0 || limitY > 0 || limitZ > 0) {
+ output.parallelStream()
+ .filter(block -> {
+ // TRUE if the location is excluded due to a limited radius
+ boolean limitCriteriaX = false, limitCriteriaY = false, limitCriteriaZ = false;
+
+ if (limitX > 0) {
+ limitCriteriaX = block.getX() > (center.getX() + limitX) || block.getX() < (center.getX() - limitX);
+ }
+
+ if (limitY > 0) {
+ limitCriteriaY = block.getY() > (center.getY() + limitY) || block.getY() < (center.getY() - limitY);
+ }
+
+ if (limitZ > 0) {
+ limitCriteriaZ = block.getZ() > (center.getZ() + limitZ) || block.getZ() < (center.getZ() - limitZ);
+ }
+
+ return limitCriteriaX || limitCriteriaY || limitCriteriaZ;
+ })
+ .forEach(output::remove);
+
+ // if only the surface is desired, the region would
+ // have a big hole on the cut sides, which is why a slice is
+ // added back
+ if (surfaceOnly) {
+ if (limitX > 0) {
+ output.addAll(getZSliceAt(center.getBlockX() + limitX));
+ output.addAll(getZSliceAt(center.getBlockX() - limitX));
+ }
+ if (limitY > 0) {
+ output.addAll(getYSliceAt(center.getBlockY() + limitY));
+ output.addAll(getYSliceAt(center.getBlockY() - limitY));
+ }
+ if (limitZ > 0) {
+ output.addAll(getXSliceAt(center.getBlockZ() + limitZ));
+ output.addAll(getXSliceAt(center.getBlockZ() - limitZ));
+ }
+ }
+
+ }
+
+ return output;
+ }
+
+ /**
+ * Gets a slice of blocks of the ellipsoid
+ * which are on a specific height/y-Axis. The
+ * slice itself will be oriented along the x-axis.
+ *
+ * @param y The absolute y value of the blocks you want to get.
+ * @return All blocks of the desired slice.
+ */
+ public Set getYSliceAt(double y) {
+ Set blocks = Sets.newConcurrentHashSet();
+ getBlocks().parallelStream()
+ .filter(block -> block.getY() == Location.locToBlock(y))
+ .forEach(blocks::add);
+ return blocks;
+ }
+
+ /**
+ * Gets a slice of blocks of the ellipsoid
+ * which are on a specific x-Axis. The
+ * slice itself will be oriented along the z-axis.
+ *
+ * @param x The absolute x value of the blocks you want to get.
+ * @return All blocks of the desired slice.
+ */
+ public Set getXSliceAt(double x) {
+ Set blocks = Sets.newConcurrentHashSet();
+ getBlocks().parallelStream()
+ .filter(block -> block.getX() == Location.locToBlock(x))
+ .forEach(blocks::add);
+ return blocks;
+ }
+
+ /**
+ * Gets a slice of blocks of the ellipsoid
+ * which are on a specific z-Axis. The
+ * slice itself will be oriented along the x-axis.
+ *
+ * @param z The absolute z value of the blocks you want to get.
+ * @return All blocks of the desired slice.
+ */
+ public Set getZSliceAt(double z) {
+ Set blocks = Sets.newConcurrentHashSet();
+ getBlocks().parallelStream()
+ .filter(block -> block.getZ() == Location.locToBlock(z))
+ .forEach(blocks::add);
+ return blocks;
+ }
+
+ /**
+ * Gets all chunks covered by this region no matter if they
+ * are loaded or not. If you only want loaded chunks,
+ * use {@link #getLoadedChunks()} instead.
+ *
+ * @return A set of all chunks covered by this region.
+ */
+ @Override
+ public Set getChunks() {
+ return toCuboid().getChunks();
+ }
+
+ /**
+ * Gets all chunks that are loaded and covered by this region.
+ * If you want to include unloaded chunks as well, use {@link #getChunks()}
+ * instead.
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @return A set of all loaded chunks covered by this region.
+ */
+ @Override
+ public Set getLoadedChunks() {
+ return toCuboid().getLoadedChunks();
+ }
+
+ /**
+ * Expands the current region by a certain multiplier.
+ * This method does not update the listeners for the region.
+ *
+ * @param amount The amount of expansion in blocks.
+ */
+ @Override
+ protected void expandIgnoreListeners(double amount) {
+ xRadius += amount;
+ yRadius += amount;
+ zRadius += amount;
+ updateMinMax();
+ }
+
+ /**
+ * Expands the current region into a specific direction by
+ * a given multiplier.
+ * This method does not update the listeners for the region.
+ *
+ * @param direction The direction to expand the region in.
+ * @param amount The amount of expansion in blocks.
+ */
+ @Override
+ protected void expandIgnoreListeners(KelpBlockFace direction, double amount) {
+ switch (direction) {
+ case UP:
+ case DOWN:
+ yRadius += amount / 2;
+ break;
+ case EAST:
+ case WEST:
+ xRadius += amount / 2;
+ break;
+ case NORTH:
+ case SOUTH:
+ zRadius += amount / 2;
+ break;
+ default:
+ throw new IllegalArgumentException("Error when expanding EllipsoidRegion: BlockFace must be one of UP, DOWN, NORTH, SOUTH, EAST, WEST");
+ }
+ updateMinMax();
+ }
+
+ /**
+ * Expands the current region into a specific direction by
+ * a given multiplier.
+ *
+ * This method automatically moves the ellipsoid up by the given
+ * amount to avoid that there will be an expansion in the opposite
+ * direction as well.
+ *
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @param direction The direction to expand the region in.
+ * @param amount The amount of expansion in blocks.
+ */
+ public void expandAndMove(KelpBlockFace direction, double amount) {
+ expand(direction, amount);
+ move(direction.getDirection().multiply(amount / 2));
+ }
+
+ /**
+ * Expands the current region in the given axis.
+ * This method does not update the listeners for the region.
+ *
+ * @param negativeX The expansion on the negative x-axis (equal to west)
+ * @param positiveX The expansion on the positive x-axis (equal to east)
+ * @param negativeY The expansion on the negative y-axis (equal to down)
+ * @param positiveY The expansion on the positive y-axis (equal to up)
+ * @param negativeZ The expansion on the negative z-axis (equal to north)
+ * @param positiveZ The expansion on the positive z-axis (equal to south)
+ */
+ @Override
+ protected void expandIgnoreListener(double negativeX, double positiveX, double negativeY, double positiveY, double negativeZ, double positiveZ) {
+ this.xRadius = positiveX + negativeX;
+ this.yRadius = positiveY + negativeY;
+ this.zRadius = positiveZ + negativeZ;
+ updateMinMax();
+ }
+
+ /**
+ * Updates the {@code minPos} and {@code maxPos} according to the
+ * radius values. This should be executed every time you expand the
+ * region for example as the radius values are updated, but the
+ * outer points remain the same, which might break the listener system.
+ */
+ private void updateMinMax() {
+ minPos = center.clone().subtract(xRadius, yRadius, zRadius);
+ maxPos = center.clone().add(xRadius, yRadius, zRadius);
+ }
+
+ /**
+ * Expands the current region in the given axis.
+ *
+ * This method automatically moves the ellipsoid up by the given
+ * amount to avoid that there will be an expansion in the opposite
+ * direction as well.
+ *
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @param negativeX The expansion on the negative x-axis (equal to west)
+ * @param positiveX The expansion on the positive x-axis (equal to east)
+ * @param negativeY The expansion on the negative y-axis (equal to down)
+ * @param positiveY The expansion on the positive y-axis (equal to up)
+ * @param negativeZ The expansion on the negative z-axis (equal to north)
+ * @param positiveZ The expansion on the positive z-axis (equal to south)
+ */
+ public void expandAndMove(double negativeX, double positiveX, double negativeY, double positiveY, double negativeZ, double positiveZ) {
+ expand(negativeX, positiveX, negativeY, positiveY, negativeZ, positiveZ);
+ move(positiveX - negativeX, positiveY - negativeY, positiveZ - negativeZ);
+ }
+
+ /**
+ * Determines whether the given location is contained by
+ * this region. This method does not check whether the world
+ * is equal but only the axis values.
+ *
+ * @param x The x-coordinate of the location to check.
+ * @param y The y-coordinate of the location to check.
+ * @param z The z-coordinate of the location to check.
+ * @return {@code true} if the location is contained by the region.
+ */
+ @Override
+ public boolean contains(double x, double y, double z) {
+ // if the location is excluded due to a limited radius
+ boolean limitCriteriaX = false, limitCriteriaY = false, limitCriteriaZ = false;
+
+ if (limitX > 0) {
+ limitCriteriaX = x > (center.getX() + limitX) || x < (center.getX() - limitX);
+ }
+
+ if (limitY > 0) {
+ limitCriteriaY = y > (center.getY() + limitY) || y < (center.getY() - limitY);
+ }
+
+ if (limitZ > 0) {
+ limitCriteriaZ = z > (center.getZ() + limitZ) || z < (center.getZ() - limitZ);
+ }
+
+ return getCostAt(x, y, z) <= 1 && !limitCriteriaX && !limitCriteriaY && !limitCriteriaZ;
+ }
+
+ /**
+ * Gets the "cost" of a block at a given location. A location's cost
+ * is defined by {@code (x/xRadius)^2 + (y/yRadius)^2 + (z/zRadius)^2},
+ * where x/y/z are defined by {@code x - center.x}, etc.
+ *
+ * This can be used to determine how far a block is away from the
+ * ellipsoid or in which layer it is roughly located. If the cost
+ * is smaller than or equal to 1, it is contained by the ellipsoid,
+ * otherwise it's not.
+ *
+ * @param location The location you want to get the cost of.
+ * @return The "cost" of the given location.
+ */
+ public double getCostAt(KelpLocation location) {
+ return getCostAt(location.getX(), location.getY(), location.getZ());
+ }
+
+ /**
+ * Gets the "cost" of a block at a given location. A location's cost
+ * is defined by {@code (x/xRadius)^2 + (y/yRadius)^2 + (z/zRadius)^2},
+ * where x/y/z are defined by {@code x - center.x}, etc.
+ *
+ * This can be used to determine how far a block is away from the
+ * ellipsoid or in which layer it is roughly located. If the cost
+ * is smaller than or equal to 1, it is contained by the ellipsoid,
+ * otherwise it's not.
+ *
+ * @param x The x-coordinate of the location you want to get the cost of.
+ * @param y The y-coordinate of the location you want to get the cost of.
+ * @param z The z-coordinate of the location you want to get the cost of.
+ * @return The "cost" of the given location.
+ */
+ public double getCostAt(double x, double y, double z) {
+ return ((x - center.getX()) / xRadius) * ((x - center.getX()) / xRadius)
+ + ((y - center.getY()) / yRadius) * ((y - center.getY()) / yRadius)
+ + ((z - center.getZ()) / zRadius) * ((z - center.getZ()) / zRadius);
+ }
+
+ /**
+ * Checks whether this ellipsoid is a sphere.
+ * For this to be true, all radius values have to be the same:
+ * {@code xRadius = yRadius = zRadius}
+ *
+ * @return {@code true} if all radius are equal.
+ */
+ public boolean isSphere() {
+ return xRadius == yRadius && xRadius == zRadius;
+ }
+
+ /**
+ * Checks whether this ellipsoid is a spheroid.
+ * For this to be true, at least two radius values have to be equal to each other.
+ *
+ * @return {@code true} if all radius are equal.
+ */
+ public boolean isSpheroid() {
+ return xRadius == yRadius || xRadius == zRadius || yRadius == zRadius;
+ }
+
+ /**
+ * Sets the radius of all axis to the given value.
+ *
+ * @param radius The radius to apply for all axis.
+ * @return An instance of the current region.
+ */
+ public EllipsoidRegion setRadius(double radius) {
+ this.xRadius = radius;
+ this.yRadius = radius;
+ this.zRadius = radius;
+ return this;
+ }
+
+ /**
+ * Sets the radius of the x axis to the given value.
+ *
+ * @param xRadius The radius to apply on the x axis.
+ * @return An instance of the current region.
+ */
+ public EllipsoidRegion setXRadius(double xRadius) {
+ this.xRadius = xRadius;
+ return this;
+ }
+
+ /**
+ * Sets the radius of the y axis to the given value.
+ *
+ * @param yRadius The radius to apply on the y axis.
+ * @return An instance of the current region.
+ */
+ public EllipsoidRegion setYRadius(double yRadius) {
+ this.yRadius = yRadius;
+ return this;
+ }
+
+ /**
+ * Sets the radius of the z axis to the given value.
+ *
+ * @param zRadius The radius to apply on the z axis.
+ * @return An instance of the current region.
+ */
+ public EllipsoidRegion setZRadius(double zRadius) {
+ this.zRadius = zRadius;
+ return this;
+ }
+
+ /**
+ * Gets the radius of this ellipsoid along the x axis.
+ * @return The radius along the x axis.
+ */
+ public double getXRadius() {
+ return xRadius;
+ }
+
+ /**
+ * Gets the radius of this ellipsoid along the y axis.
+ * @return The radius along the y axis.
+ */
+ public double getYRadius() {
+ return yRadius;
+ }
+
+ /**
+ * Gets the radius of this ellipsoid along the z axis.
+ * @return The radius along the z axis.
+ */
+ public double getZRadius() {
+ return zRadius;
+ }
+
+ @Override
+ public KelpRegion clone() {
+ return EllipsoidRegion.create(this.center, xRadius, yRadius, zRadius);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof EllipsoidRegion)) {
+ return false;
+ }
+
+ EllipsoidRegion region = (EllipsoidRegion) object;
+ return region.getWorldName().equalsIgnoreCase(worldName)
+ && region.getXRadius() == this.getXRadius()
+ && region.getYRadius() == this.getYRadius()
+ && region.getZRadius() == this.getZRadius()
+ && region.getCenter().equals(this.center);
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(getWorldName())
+ .append("ELLIPSOID")
+ .append(xRadius)
+ .append(xRadius)
+ .append(yRadius)
+ .append(center.hashCode())
+ .toHashCode();
+ }
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/region/KelpRegion.java b/core/src/main/java/de/pxav/kelp/core/world/region/KelpRegion.java
new file mode 100644
index 00000000..6282653a
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/world/region/KelpRegion.java
@@ -0,0 +1,487 @@
+package de.pxav.kelp.core.world.region;
+
+import de.pxav.kelp.core.KelpPlugin;
+import de.pxav.kelp.core.event.kelpevent.region.PlayerEnterRegionEvent;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.world.KelpBlock;
+import de.pxav.kelp.core.world.KelpChunk;
+import de.pxav.kelp.core.world.KelpLocation;
+import de.pxav.kelp.core.world.KelpWorld;
+import de.pxav.kelp.core.world.util.KelpBlockFace;
+import org.bukkit.util.Vector;
+
+import javax.inject.Singleton;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * A KelpRegion can be any collection of blocks in a world that are grouped
+ * together as one. This can be of different shapes: for example a cube or a sphere.
+ *
+ * A region can be expanded and moved in the 3D-space and you can do operations
+ * with its blocks.
+ *
+ * You can also listen for region events, for example the {@link PlayerEnterRegionEvent},
+ * which is triggered each time a player enters a registered region. For a region to
+ * be recognized by listeners, you have to enable listeners for it using
+ * {@link #enableListeners()}, which can be disabled at every time using
+ * {@link #disableListeners()}.
+ *
+ * There are different sub-types of KelpRegions, each of which represent different
+ * shapes and implement own features such as the simple {@link CuboidRegion} or the
+ * {@link EllipsoidRegion}.
+ *
+ * @author pxav
+ */
+@Singleton
+public abstract class KelpRegion implements Cloneable {
+
+ protected KelpRegionRepository regionRepository;
+
+ // the region id used by the listener system for simplified
+ protected UUID regionId = UUID.randomUUID();
+
+ // the name of the world this region is located in
+ protected String worldName;
+
+ // the lowermost corner of the region with the lowest axis values
+ protected KelpLocation minPos;
+
+ // the uppermost corner of the region with the highest axis values
+ protected KelpLocation maxPos;
+
+ public KelpRegion(KelpRegionRepository regionRepository) {
+ this.regionRepository = regionRepository;
+ }
+
+ /**
+ * Moves the region into the direction of the given
+ * {@link Vector}.
+ *
+ * @param vector The vector providing the direction and
+ * power of the movement.
+ */
+ public void move(Vector vector) {
+ this.disableListeners();
+ this.moveIgnoreListeners(vector);
+ this.enableListeners();
+ }
+
+ /**
+ * Moves the region into the direction of the given
+ * {@link Vector}.
+ *
+ * @param vector The vector providing the direction and
+ * power of the movement.
+ */
+ protected abstract void moveIgnoreListeners(Vector vector);
+
+ /**
+ * Moves the region into a certain direction defined by
+ * the different axis values.
+ *
+ * @param dx How far the region should be moved on the x-axis
+ * @param dy How far the region should be moved on the y-axis
+ * @param dz How far the region should be moved on the z-axis
+ */
+ public void move(double dx, double dy, double dz) {
+ this.disableListeners();
+ this.moveIgnoreListeners(dx, dy, dz);
+ this.enableListeners();
+ }
+
+ /**
+ * Moves the region into a certain direction defined by
+ * the different axis values.
+ *
+ * @param dx How far the region should be moved on the x-axis
+ * @param dy How far the region should be moved on the y-axis
+ * @param dz How far the region should be moved on the z-axis
+ */
+ protected abstract void moveIgnoreListeners(double dx, double dy, double dz);
+
+ /**
+ * Gets the total volume of this region, which depends
+ * on the region's shape. This method calculates the exact
+ * value and should be used if your region is not exactly
+ * based on block locations. Otherwise, use {@link #getBlockVolume()}.
+ *
+ * @return The exact volume of this region in blocks.
+ */
+ public abstract double getVolume();
+
+ /**
+ * Gets the total volume of this region, which depends
+ * on the region's shape. This method uses the integer block
+ * location values and is therefore an approximation of {@link #getVolume()}
+ * in most cases.
+ *
+ * @return The approximate volume of this location in blocks.
+ */
+ public abstract int getBlockVolume();
+
+ /**
+ * Gets the center of the given region shape. The exact location
+ * of the center depends on the implementation, but in most cases
+ * it is the cubic center of a region.
+ *
+ * @return The center location of this region.
+ */
+ public abstract KelpLocation getCenter();
+
+ /**
+ * Gets a set of all blocks which are on the regions surface.
+ *
+ * @return A set of blocks containing all blocks on the regions surface.
+ */
+ public abstract Set getSurfaceBlocks();
+
+ /**
+ * Gets all blocks contained by this region.
+ * This can be used to visualize its shape.
+ *
+ * @return A set of all blocks contained by this region.
+ */
+ public abstract Set getBlocks();
+
+ /**
+ * Gets all chunks covered by this region no matter if they
+ * are loaded or not. If you only want loaded chunks,
+ * use {@link #getLoadedChunks()} instead.
+ *
+ * @return A set of all chunks covered by this region.
+ */
+ public abstract Set getChunks();
+
+ /**
+ * Gets all chunks that are loaded and covered by this region.
+ * If you want to include unloaded chunks as well, use {@link #getChunks()}
+ * instead.
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @return A set of all loaded chunks covered by this region.
+ */
+ public abstract Set getLoadedChunks();
+
+ /**
+ * Expands the current region by a certain multiplier.
+ *
+ * @param amount The amount of expansion in blocks.
+ */
+ public void expand(double amount) {
+ this.disableListeners();
+ this.expandIgnoreListeners(amount);
+ this.enableListeners();
+ }
+
+ /**
+ * Expands the current region by a certain multiplier.
+ * This method does not update the listeners for the region.
+ *
+ * @param amount The amount of expansion in blocks.
+ */
+ protected abstract void expandIgnoreListeners(double amount);
+
+ /**
+ * Expands the current region into a specific direction by
+ * a given multiplier.
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @param direction The direction to expand the region in.
+ * @param amount The amount of expansion in blocks.
+ */
+ public void expand(KelpBlockFace direction, double amount) {
+ this.disableListeners();
+ this.expandIgnoreListeners(direction, amount);
+ this.enableListeners();
+ }
+
+ /**
+ * Expands the current region into a specific direction by
+ * a given multiplier.
+ * This method does not update the listeners for the region.
+ *
+ * @param direction The direction to expand the region in.
+ * @param amount The amount of expansion in blocks.
+ */
+ protected abstract void expandIgnoreListeners(KelpBlockFace direction, double amount);
+
+ /**
+ * Expands the current region in the given axis.
+ * This method automatically applies the changes to the listener system
+ * if it is enabled for this region.
+ *
+ * @param negativeX The expansion on the negative x-axis (equal to west)
+ * @param positiveX The expansion on the positive x-axis (equal to east)
+ * @param negativeY The expansion on the negative y-axis (equal to down)
+ * @param positiveY The expansion on the positive y-axis (equal to up)
+ * @param negativeZ The expansion on the negative z-axis (equal to north)
+ * @param positiveZ The expansion on the positive z-axis (equal to south)
+ */
+ public void expand(double negativeX,
+ double positiveX,
+ double negativeY,
+ double positiveY,
+ double negativeZ,
+ double positiveZ) {
+ this.disableListeners();
+ this.expandIgnoreListener(negativeX, positiveX, negativeY, positiveY, negativeZ, positiveZ);
+ this.enableListeners();
+ }
+
+ /**
+ * Expands the current region in the given axis.
+ * This method does not update the listeners for the region.
+ *
+ * @param negativeX The expansion on the negative x-axis (equal to west)
+ * @param positiveX The expansion on the positive x-axis (equal to east)
+ * @param negativeY The expansion on the negative y-axis (equal to down)
+ * @param positiveY The expansion on the positive y-axis (equal to up)
+ * @param negativeZ The expansion on the negative z-axis (equal to north)
+ * @param positiveZ The expansion on the positive z-axis (equal to south)
+ */
+ protected abstract void expandIgnoreListener(double negativeX,
+ double positiveX,
+ double negativeY,
+ double positiveY,
+ double negativeZ,
+ double positiveZ);
+
+ /**
+ * Determines whether the given location is contained by
+ * this region. This method does not check whether the world
+ * is equal but only the axis values.
+ *
+ * @param x The x-coordinate of the location to check.
+ * @param y The y-coordinate of the location to check.
+ * @param z The z-coordinate of the location to check.
+ * @return {@code true} if the location is contained by the region.
+ */
+ public abstract boolean contains(double x, double y, double z);
+
+ /**
+ * Checks whether the given location is contained by this region.
+ * This method also checks if the worlds are equal.
+ *
+ * @param location The location to check for.
+ * @return {@code true} if the location is contained by this region.
+ */
+ public boolean contains(KelpLocation location) {
+ if (!worldName.equalsIgnoreCase(location.getWorldName())) {
+ return false;
+ }
+ return contains(location.getX(), location.getZ(), location.getZ());
+ }
+
+ /**
+ * Checks whether this region contains the given
+ * {@link KelpPlayer}.
+ *
+ * @param player The player you want to check.
+ * @return {@code true} if the player is contained by this region.
+ */
+ public boolean contains(KelpPlayer player) {
+ return contains(player.getLocation());
+ }
+
+ /**
+ * Checks whether this region contains the given
+ * {@link KelpBlock}.
+ *
+ * @param block The block you want to check.
+ * @return {@code true} if the block is contained by this region.
+ */
+ public boolean contains(KelpBlock block) {
+ return contains(block.getLocation());
+ }
+
+ /**
+ * Gets the approximate x, y, and z dimensions of this region.
+ * So it basically measures how long the region is on a
+ * specific axis of the world in blocks without fraction digits.
+ * If you want to get the exact dimensions, use {@link #getDimensions()}
+ * instead.
+ *
+ * @return The dimensions of this region in the format [x, y, z]
+ */
+ public int[] getBlockDimensions() {
+ return new int[] {
+ maxPos.getBlockX() - minPos.getBlockX() + 1,
+ maxPos.getBlockY() - minPos.getBlockY() + 1,
+ maxPos.getBlockZ() - minPos.getBlockZ() + 1
+ };
+ }
+
+ /**
+ * Gets the exact x, y, and z dimensions of this region.
+ * So it basically measures how long the region is on a
+ * specific axis of the world in blocks.
+ *
+ * @return The dimensions of this region in the format [x, y, z]
+ */
+ public double[] getDimensions() {
+ return new double[] {
+ maxPos.getX() - minPos.getX() + 1,
+ maxPos.getY() - minPos.getY() + 1,
+ maxPos.getZ() - minPos.getZ() + 1
+ };
+ }
+
+ /**
+ * Gets the cubic outer corners of this region. It basically
+ * interpolates the missing corners based on the uppermost
+ * and lowermost corners ({@link #getMaxPos()} and {@link #getMinPos()}.
+ *
+ * @return An array containing all outer cubic corners of this region.
+ */
+ public KelpLocation[] getOuterCorners() {
+ return new KelpLocation[] {
+ minPos,
+ maxPos,
+ KelpLocation.from(getWorld().getName(), minPos.getX(), minPos.getY(), maxPos.getZ()),
+ KelpLocation.from(getWorld().getName(), minPos.getX(), maxPos.getY(), minPos.getZ()),
+ KelpLocation.from(getWorld().getName(), maxPos.getX(), minPos.getY(), minPos.getZ()),
+ KelpLocation.from(getWorld().getName(), minPos.getX(), maxPos.getY(), maxPos.getZ()),
+ KelpLocation.from(getWorld().getName(), maxPos.getX(), maxPos.getY(), minPos.getZ()),
+ KelpLocation.from(getWorld().getName(), maxPos.getX(), minPos.getY(), maxPos.getZ())
+ };
+ }
+
+ /**
+ * Gets the name of the world this region is located in.
+ * @return
+ */
+ public String getWorldName() {
+ return worldName;
+ }
+
+ /**
+ * Sets the name of the world this region is located in.
+ *
+ * @param worldName The name of this region's world.
+ */
+ public void setWorldName(String worldName) {
+ this.worldName = worldName;
+ }
+
+ /**
+ * Gets the {@link KelpWorld} this region is located in.
+ *
+ * @return The world this region is located in.
+ * {@code null} if the world has not been set.
+ */
+ public KelpWorld getWorld() {
+ if (this.worldName == null) {
+ return null;
+ }
+ return KelpWorld.from(worldName);
+ }
+
+ /**
+ * Gets the lowermost point of this region with
+ * the lowest axis values.
+ *
+ * @return The lowermost point of this region.
+ */
+ public KelpLocation getMinPos() {
+ return minPos;
+ }
+
+ /**
+ * Gets the uppermost point of this region with
+ * the highest axis values.
+ *
+ * @return The uppermost point of this region.
+ */
+ public KelpLocation getMaxPos() {
+ return maxPos;
+ }
+
+ /**
+ * Converts and clones this region into a cuboid region using its min and max
+ * pos as outer corners.
+ *
+ * @return The cuboid region based on the min and max pos of this region.
+ */
+ public CuboidRegion toCuboid() {
+ return CuboidRegion.create(minPos, maxPos);
+ }
+
+ /**
+ * Converts and clones this region into an ellipsoid region, which uses
+ * the given min and max pos as outer corners, so that the ellipsoid
+ * fits into the cube with maximum size.
+ *
+ * @return The {@link EllipsoidRegion} based on the min and max pos of this region.
+ */
+ public EllipsoidRegion toEllipsoid() {
+ return EllipsoidRegion.create(minPos, maxPos);
+ }
+
+ /**
+ * Gets the unique id for this region. This id can be
+ * used to identify regions without having to compare dynamic values
+ * such as their corners/etc. this ensures increased consistency.
+ *
+ * @return The region to get the unique id of.
+ */
+ public UUID getRegionId() {
+ return regionId;
+ }
+
+ /**
+ * Enables all listeners for this region.
+ * This means that events such as {@link PlayerEnterRegionEvent}
+ * will be triggered. Region listeners might become relatively
+ * performance intensive if you have many of them, so they are
+ * disabled by default.
+ *
+ * Always call this method first if you plan to use them.
+ * They can be disabled at any time using {@link #disableListeners()}
+ */
+ public void enableListeners() {
+ if (listenersEnabled()) {
+ return;
+ }
+ regionRepository.listenTo(this);
+ }
+
+ /**
+ * Disables all listeners for this region.
+ * This means that events such as {@link PlayerEnterRegionEvent}
+ * won't be triggered anymore, which saves performance.
+ *
+ * Disabled listeners are the default for all regions.
+ */
+ public void disableListeners() {
+ if (!listenersEnabled()) {
+ return;
+ }
+ regionRepository.stopListeningTo(this);
+ }
+
+ /**
+ * Checks whether listeners are currently enabled for this region.
+ * Listeners are responsible for triggering events such as
+ * {@link PlayerEnterRegionEvent}
+ *
+ * @return {@code true} if listeners are enabled.
+ */
+ public boolean listenersEnabled() {
+ return regionRepository.isListeningTo(this);
+ }
+
+ @Override
+ public abstract KelpRegion clone();
+
+ public abstract boolean equals(Object object);
+
+ public abstract int hashCode();
+
+ protected static KelpRegionRepository getRegionRepository() {
+ return KelpPlugin.getInjector().getInstance(KelpRegionRepository.class);
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/region/KelpRegionRepository.java b/core/src/main/java/de/pxav/kelp/core/world/region/KelpRegionRepository.java
new file mode 100644
index 00000000..7802d00a
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/world/region/KelpRegionRepository.java
@@ -0,0 +1,197 @@
+package de.pxav.kelp.core.world.region;
+
+import de.pxav.kelp.core.common.ConcurrentMultimap;
+import de.pxav.kelp.core.common.ConcurrentSetMultimap;
+import de.pxav.kelp.core.event.kelpevent.region.PlayerEnterRegionEvent;
+import de.pxav.kelp.core.event.kelpevent.region.PlayerLeaveRegionEvent;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.scheduler.KelpSchedulerRepository;
+import de.pxav.kelp.core.scheduler.type.RepeatingScheduler;
+import de.pxav.kelp.core.world.util.ApproximateLocation;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.player.PlayerJoinEvent;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.UUID;
+
+/**
+ * This repository class is used to manage {@link KelpRegion}s and their listeners.
+ *
+ * The listener system is optimized for maximum performance: Only the regions you need
+ * the listeners for are really listened by calling {@link KelpRegion#enableListeners()}.
+ * Furthermore Kelp does not execute the {@code contains} check for every player and every
+ * region, but only for the regions which are in the range of the player. This massively
+ * reduces the required computing power for each listener iteration. Look at
+ * {@link ApproximateLocation} for more detail.
+ *
+ * @author pxav
+ */
+@Singleton
+public class KelpRegionRepository {
+
+ // approximate location -> all regions within the area of the approx location
+ private ConcurrentSetMultimap nearRegions;
+
+ // player -> region id
+ private ConcurrentMultimap containedBy;
+
+ // the task id of the listener scheduler
+ private UUID task;
+
+ // scheduler repository for interrupting the task
+ private KelpSchedulerRepository schedulerRepository;
+
+ @Inject
+ public KelpRegionRepository(KelpSchedulerRepository schedulerRepository) {
+ this.containedBy = ConcurrentSetMultimap.create();
+ this.nearRegions = ConcurrentSetMultimap.create();
+
+ this.schedulerRepository = schedulerRepository;
+ }
+
+ /**
+ * Enables listeners for the given region. This method caches the approximate
+ * location of the given region, so it should be re-registered every time
+ * those approximate locations change (on move/expand).
+ *
+ * This method automatically activates the listener scheduler if there
+ * is currently no running.
+ *
+ * @param region The region to enable the listeners for.
+ */
+ void listenTo(KelpRegion region) {
+ // go through all coordinates on the x/z plane and add their approximate
+ // location to the map.
+ for (int x = region.getMinPos().getBlockX(); x <= region.getMaxPos().getBlockX(); x++) {
+ for (int z = region.getMinPos().getBlockZ(); z <= region.getMaxPos().getBlockZ(); z++) {
+ ApproximateLocation current = ApproximateLocation.fromExact(region.getWorldName(), x, z);
+ nearRegions.put(current, region);
+ }
+ }
+
+ // enable scheduler if there is no one running
+ if (!isListenerRunning()) {
+ startListenerTasks();
+ }
+ }
+
+ /**
+ * Disables listeners for the given region. This will remove
+ * all entries in the cache for this region.
+ *
+ * @param region The region to remove the listener of.
+ */
+ void stopListeningTo(KelpRegion region) {
+ for (int x = region.getMinPos().getBlockX(); x <= region.getMaxPos().getBlockX(); x++) {
+ for (int z = region.getMinPos().getBlockZ(); z <= region.getMaxPos().getBlockZ(); z++) {
+ ApproximateLocation current = ApproximateLocation.fromExact(region.getWorldName(), x, z);
+ nearRegions.remove(current, region);
+ }
+ }
+ }
+
+ /**
+ * Checks whether the given region has listeners enabled.
+ *
+ * @param region The region you want to check for.
+ * @return {@code true} the region to check for.
+ */
+ boolean isListeningTo(KelpRegion region) {
+ return nearRegions.containsValue(region);
+ }
+
+ /**
+ * This listener checks if there is any region having listeners enabled
+ * and starts the listener scheduler if needed.
+ *
+ * This is needed because the scheduler is shut down automatically if no
+ * players are online to save performance. Hence, it has to be started when a player
+ * joins.
+ *
+ * @param event The event to listen for.
+ */
+ @EventHandler
+ public void handlePlayerJoin(PlayerJoinEvent event) {
+ // check if there is any region to listen for and if the listeners are really shut down.
+ if (nearRegions.isEmpty() || this.isListenerRunning()) {
+ return;
+ }
+
+ this.startListenerTasks();
+
+ }
+
+ /**
+ * Starts the region listener task. This task automatically
+ * checks when to shut down the listeners.
+ */
+ private void startListenerTasks() {
+ task = RepeatingScheduler.create()
+ .async()
+ .every(100)
+ .milliseconds()
+ .waitForTaskCompletion(true)
+ .run(taskId -> {
+
+ // if there are no regions to listen to anymore, cancel the scheduler
+ if (nearRegions.isEmpty() || Bukkit.getOnlinePlayers().size() == 0) {
+ stopListenerTasks();
+ }
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ // iterate each region in range of the player
+ nearRegions.getOrEmpty(ApproximateLocation.from(player.getLocation())).forEach(region -> {
+
+ // if the region world differs from the player world, the player cannot
+ // be contained by the region.
+ if (!region.getWorldName().equalsIgnoreCase(player.getWorld().getName())) {
+ return;
+ }
+
+ boolean contains = region.contains(
+ player.getLocation().getX(),
+ player.getLocation().getY(),
+ player.getLocation().getZ()
+ );
+
+ // if player is in region but was not in the region before -> Enter
+ if (contains && !containedBy.containsEntry(player.getUniqueId(), region.getRegionId())) {
+ this.containedBy.put(player.getUniqueId(), region.getRegionId());
+ Bukkit.getPluginManager().callEvent(new PlayerEnterRegionEvent(region, KelpPlayer.from(player)));
+
+ // if the player is not in region but was in there before -> Exit
+ } else if (!contains && containedBy.containsEntry(player.getUniqueId(), region.getRegionId())) {
+ this.containedBy.remove(player.getUniqueId(), region.getRegionId());
+ Bukkit.getPluginManager().callEvent(new PlayerLeaveRegionEvent(region, KelpPlayer.from(player)));
+ }
+ });
+ }
+ });
+ }
+
+
+ /**
+ * Stops the listener task and sets the listener id back to {@code null}.
+ * This can be used if there is no region to listen for anymore
+ * for example and therefore to save performance.
+ */
+ private void stopListenerTasks() {
+ if (task != null) {
+ schedulerRepository.interruptScheduler(task);
+ task = null;
+ }
+ }
+
+ /**
+ * Checks whether the listener scheduler is currently running.
+ *
+ * @return {@code true} if the listener task is currently running.
+ */
+ private boolean isListenerRunning() {
+ return task != null;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/util/ApproximateLocation.java b/core/src/main/java/de/pxav/kelp/core/world/util/ApproximateLocation.java
new file mode 100644
index 00000000..121c1910
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/world/util/ApproximateLocation.java
@@ -0,0 +1,153 @@
+package de.pxav.kelp.core.world.util;
+
+import de.pxav.kelp.core.world.KelpLocation;
+import de.pxav.kelp.core.world.KelpWorld;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.bukkit.Location;
+
+/**
+ * An approximate location is an object to save rough locations in a world.
+ * It is similar to a chunk, which covers a 16x16 space with the main difference
+ * being that this location type has a scale of 100x100.
+ *
+ * So if you get the x and z of an approximate location, {@code x=1} will mean
+ * that the exact location is {@code 200 > x >= 100}. This is useful if you want to execute
+ * certain operations for players in a certain area without having to use performance-heavy(er)
+ * {@code distance()} functions. This is used in the built-in region system for example.
+ * The region enter and exit event don't check each region on a world to check if a player
+ * is in there, but only for regions with the same approximate location. This saves lots of
+ * performance.
+ *
+ * @author pxav
+ */
+public class ApproximateLocation {
+
+ private int x;
+ private int z;
+ private String worldName;
+
+ /**
+ * Converts the given {@link KelpLocation} into an {@link ApproximateLocation}.
+ * This does not have an effect on the given source location.
+ *
+ * @param location The source location to create the approximate location from.
+ * @return The approximate location equivalent to the given {@link KelpLocation}.
+ */
+ public static ApproximateLocation from(KelpLocation location) {
+ return new ApproximateLocation(location.getWorldName(), location.getBlockX(), location.getBlockZ());
+ }
+
+ /**
+ * Converts the given {@link KelpLocation} into an {@link ApproximateLocation}.
+ * This does not have an effect on the given source location.
+ *
+ * @param location The source location to create the approximate location from.
+ * @return The approximate location equivalent to the given {@link Location bukkit location}.
+ */
+ public static ApproximateLocation from(Location location) {
+ return new ApproximateLocation(location.getWorld().getName(), location.getBlockX(), location.getBlockZ());
+ }
+
+ /**
+ * Creates a new approximate location based on the given world and x and z axis.
+ *
+ * @param worldName The name of the world for the desired location.
+ * @param x The exact x axis of the original location.
+ * @param z The exact z axis of the original location.
+ * @return The final approximate location equivalent to the given location.
+ */
+ public static ApproximateLocation fromExact(String worldName, int x, int z) {
+ return new ApproximateLocation(worldName, x, z);
+ }
+
+ /**
+ * Creates a new approximate location based on the given world and x and z axis.
+ *
+ * @param world The world of the desired location.
+ * @param x The exact x axis of the original location.
+ * @param z The exact z axis of the original location.
+ * @return The final approximate location equivalent to the given location.
+ */
+ public static ApproximateLocation fromExact(KelpWorld world, int x, int z) {
+ return fromExact(world.getName(), x, z);
+ }
+
+ /**
+ * Creates a new approximate location based on a world name and the
+ * approximate x and z values. If you want to use exact values to calculate
+ * the approximate ones, use {@link #fromExact(String, int, int)} instead.
+ *
+ * @param worldName The name of the world for this location.
+ * @param x The approximate x value to use (won't be further converted)
+ * @param z The approximate x value to use (won't be further converted)
+ * @return The final approximate location based on the given data.
+ */
+ public static ApproximateLocation create(String worldName, int x, int z) {
+ return new ApproximateLocation(worldName, x, z);
+ }
+
+ private ApproximateLocation(String worldName, int x, int z) {
+ this.x = x / 100;
+ this.z = z / 100;
+ this.worldName = worldName;
+ }
+
+ /**
+ * Gets the x-value in an approximate grid, which is equal to
+ * the exact location's X divided by 100.
+ *
+ * @return The exact x axis divided by 100.
+ */
+ public int getX() {
+ return x;
+ }
+
+ /**
+ * Gets the z-value in an approximate grid, which is equal to
+ * the exact location's Z divided by 100.
+ *
+ * @return The exact z axis divided by 100.
+ */
+ public int getZ() {
+ return z;
+ }
+
+ /**
+ * Gets the name of the world of this location.
+ *
+ * @return This location's world name.
+ */
+ public String getWorldName() {
+ return worldName;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder(17, 37)
+ .append(this.x)
+ .append(this.z)
+ .append(this.worldName)
+ .toHashCode();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof ApproximateLocation)) {
+ return false;
+ }
+ ApproximateLocation location = (ApproximateLocation) object;
+
+ return location.getWorldName().equalsIgnoreCase(this.worldName)
+ && location.getX() == this.getX()
+ && location.getZ() == this.getZ();
+ }
+
+ @Override
+ public String toString() {
+ return "ApproximateLocation{" +
+ "x=" + x +
+ ", z=" + z +
+ ", worldName='" + worldName + '\'' +
+ '}';
+ }
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/util/KelpBlockFace.java b/core/src/main/java/de/pxav/kelp/core/world/util/KelpBlockFace.java
new file mode 100644
index 00000000..cbd9e37b
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/world/util/KelpBlockFace.java
@@ -0,0 +1,168 @@
+package de.pxav.kelp.core.world.util;
+
+import de.pxav.kelp.core.world.KelpBlock;
+import org.bukkit.block.BlockFace;
+import org.bukkit.util.Vector;
+
+/**
+ * Represents the face of a given block. This face can point
+ * in a cardinal direction such as {@code NORTH} or {@code SOUTH}
+ * or around the y-axis with {@link KelpBlockFace#UP} and {@link KelpBlockFace#DOWN}.
+ *
+ * This can be used to get neighbouring blocks of another block for example
+ * with {@link KelpBlock#getBlockBelow()}.
+ *
+ * @author pxav
+ */
+public enum KelpBlockFace {
+
+ NORTH(0, 0, -1),
+ EAST(1, 0, 0),
+ SOUTH(0, 0, 1),
+ WEST(-1, 0, 0),
+ UP(0, 1, 0),
+ DOWN(0, -1, 0),
+ NORTH_EAST(NORTH, EAST),
+ NORTH_WEST(NORTH, WEST),
+ SOUTH_EAST(SOUTH, EAST),
+ SOUTH_WEST(SOUTH, WEST),
+ WEST_NORTH_WEST(WEST, NORTH_WEST),
+ NORTH_NORTH_WEST(NORTH, NORTH_WEST),
+ NORTH_NORTH_EAST(NORTH, NORTH_EAST),
+ EAST_NORTH_EAST(EAST, NORTH_EAST),
+ EAST_SOUTH_EAST(EAST, SOUTH_EAST),
+ SOUTH_SOUTH_EAST(SOUTH, SOUTH_EAST),
+ SOUTH_SOUTH_WEST(SOUTH, SOUTH_WEST),
+ WEST_SOUTH_WEST(WEST, SOUTH_WEST),
+
+ /**
+ * Represents the current block and no specific face of it.
+ */
+ SELF(0, 0, 0);
+
+ private final int deltaX;
+ private final int deltaY;
+ private final int deltaZ;
+
+ public static KelpBlockFace from(BlockFace blockFace) {
+ return KelpBlockFace.valueOf(blockFace.name());
+ }
+
+ KelpBlockFace(int deltaX, int deltaY, int deltaZ) {
+ this.deltaX = deltaX;
+ this.deltaY = deltaY;
+ this.deltaZ = deltaZ;
+ }
+
+ KelpBlockFace(KelpBlockFace face1, KelpBlockFace face2) {
+ this.deltaX = face1.getDeltaX() + face2.getDeltaX();
+ this.deltaY = face1.getDeltaY() + face2.getDeltaY();
+ this.deltaZ = face1.getDeltaZ() + face2.getDeltaZ();
+ }
+
+ /**
+ * Gets the difference from the original block to the
+ * current block face in the x direction.
+ *
+ * @return The distance from the original block to the current face on the x-axis.
+ */
+ public int getDeltaX() {
+ return this.deltaX;
+ }
+
+ /**
+ * Gets the difference from the original block to the
+ * current block face in the y direction.
+ *
+ * @return The distance from the original block to the current face on the y-axis.
+ */
+ public int getDeltaY() {
+ return this.deltaY;
+ }
+
+ /**
+ * Gets the difference from the original block to the
+ * current block face in the z direction.
+ *
+ * @return The distance from the original block to the current face on the z-axis.
+ */
+ public int getDeltaZ() {
+ return this.deltaZ;
+ }
+
+ /**
+ * Converts the current block face into a {@link Vector}.
+ * This vector will point in the cardinal direction of the face.
+ *
+ * @return A vector pointing in the direction of the current block face.
+ */
+ public Vector getDirection() {
+ Vector direction = new Vector(this.deltaX, this.deltaY, this.deltaZ);
+ if (this.deltaX != 0 || this.deltaY != 0 || this.deltaZ != 0) {
+ direction.normalize();
+ }
+
+ return direction;
+ }
+
+ /**
+ * Gets the block face that is opposite of the current block face.
+ * If the current block face is {@code NORTH} for example, this will return
+ * {@code SOUTH}. If it is {@code NORTH_EAST}, it will return {@code SOUTH_WEST} and so on.
+ *
+ * @return The block face opposite to the current block face.
+ */
+ public KelpBlockFace getOppositeFace() {
+ switch(this) {
+ case NORTH:
+ return SOUTH;
+ case EAST:
+ return WEST;
+ case SOUTH:
+ return NORTH;
+ case WEST:
+ return EAST;
+ case UP:
+ return DOWN;
+ case DOWN:
+ return UP;
+ case NORTH_EAST:
+ return SOUTH_WEST;
+ case NORTH_WEST:
+ return SOUTH_EAST;
+ case SOUTH_EAST:
+ return NORTH_WEST;
+ case SOUTH_WEST:
+ return NORTH_EAST;
+ case WEST_NORTH_WEST:
+ return EAST_SOUTH_EAST;
+ case NORTH_NORTH_WEST:
+ return SOUTH_SOUTH_EAST;
+ case NORTH_NORTH_EAST:
+ return SOUTH_SOUTH_WEST;
+ case EAST_NORTH_EAST:
+ return WEST_SOUTH_WEST;
+ case EAST_SOUTH_EAST:
+ return WEST_NORTH_WEST;
+ case SOUTH_SOUTH_EAST:
+ return NORTH_NORTH_WEST;
+ case SOUTH_SOUTH_WEST:
+ return NORTH_NORTH_EAST;
+ case WEST_SOUTH_WEST:
+ return EAST_NORTH_EAST;
+ default:
+ return SELF;
+ }
+ }
+
+ /**
+ * Converts the current block face into a block face of
+ * the bukkit library.
+ *
+ * @return The bukkit block face equivalent to the current block face.
+ */
+ public BlockFace getBukkitFace() {
+ return BlockFace.valueOf(this.toString());
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/world/version/BlockVersionTemplate.java b/core/src/main/java/de/pxav/kelp/core/world/version/BlockVersionTemplate.java
index acf50caf..a8b6c03c 100644
--- a/core/src/main/java/de/pxav/kelp/core/world/version/BlockVersionTemplate.java
+++ b/core/src/main/java/de/pxav/kelp/core/world/version/BlockVersionTemplate.java
@@ -5,6 +5,7 @@
import de.pxav.kelp.core.world.KelpBlock;
import de.pxav.kelp.core.world.KelpChunk;
import de.pxav.kelp.core.world.KelpLocation;
+import de.pxav.kelp.core.world.util.KelpBlockFace;
import org.bukkit.block.BlockFace;
/**
@@ -72,6 +73,6 @@ public abstract class BlockVersionTemplate {
* @param block The block you want to simulate the application of.
* @param blockFace The face of the block to apply the bone meal to.
*/
- public abstract void applyBoneMeal(KelpBlock block, BlockFace blockFace);
+ public abstract void applyBoneMeal(KelpBlock block, KelpBlockFace blockFace);
}
diff --git a/core/src/test/java/de/pxav/kelp/core/test/common/ConcurrentListMultimapTest.java b/core/src/test/java/de/pxav/kelp/core/test/common/ConcurrentListMultimapTest.java
new file mode 100644
index 00000000..47b5b20b
--- /dev/null
+++ b/core/src/test/java/de/pxav/kelp/core/test/common/ConcurrentListMultimapTest.java
@@ -0,0 +1,91 @@
+package de.pxav.kelp.core.test.common;
+
+import com.google.common.collect.Maps;
+import de.pxav.kelp.core.common.ConcurrentListMultimap;
+import de.pxav.kelp.core.common.ConcurrentMultimap;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+/**
+ * This class is a basic test for the features of a list-based
+ * concurrent multimap provided by Kelp, {@link ConcurrentListMultimap} to be
+ * specific.
+ *
+ * @author pxav
+ */
+public class ConcurrentListMultimapTest {
+
+ private ConcurrentMultimap map;
+
+ /**
+ * Refreshes the instance of the map before each test
+ * so we get neutral and unaffected results in each test.
+ */
+ @Before
+ public void initMap() {
+ this.map = ConcurrentListMultimap.create();
+ }
+
+ /**
+ * Tests basic insert operations of the map including:
+ * - {@link ConcurrentListMultimap#put(Object, Object)}
+ * - {@link ConcurrentListMultimap#putAll(Map)}
+ */
+ @Test
+ public void testInsert() {
+ Map killStreak = Maps.newHashMap();
+ killStreak.put("player1", 3);
+ killStreak.put("player2", 0);
+ killStreak.put("player3", 5);
+
+ this.map.putAll(killStreak);
+ this.map.put("player2", 0);
+ Assert.assertEquals(this.map.size(), 4);
+
+ this.map.put("player4", 6);
+ Assert.assertEquals(this.map.size(), 5);
+ }
+
+ /**
+ * Tests all {@code contains()} methods of the multimap.
+ * This includes checking for keys, values and whole entries.
+ */
+ @Test
+ public void testContains() {
+ this.map.put("player2", 0);
+ this.map.put("player2", 1);
+ this.map.put("player3", 4);
+
+ Assert.assertFalse(this.map.containsKey("soos"));
+ Assert.assertFalse(this.map.containsValue(6));
+ Assert.assertFalse(this.map.containsEntry("player3", 1));
+
+ Assert.assertTrue(this.map.containsValue(0));
+ Assert.assertTrue(this.map.containsKey("player2"));
+ Assert.assertTrue(this.map.containsEntry("player2", 0));
+ Assert.assertTrue(this.map.containsEntry("player2", 1));
+ }
+
+ /**
+ * Tests the thread-safety of the multimap. In a normal
+ * map, a {@link java.util.ConcurrentModificationException} would
+ * occur if you iterate through a map and modify it at the same time,
+ * which does not happen if you use a concurrent multimap.
+ */
+ @Test
+ public void testConcurrentModificationException() {
+ this.map.put("1", 2);
+ this.map.put("1", 3);
+ this.map.put("2", 24);
+
+ // test whether concurrent modification exception can occur
+ this.map.forEach((player, streak) -> this.map.remove(player, streak));
+
+ // test whether all items have been removed
+ Assert.assertEquals(this.map.size(), 0);
+ }
+
+}
diff --git a/kelp-sql/pom.xml b/kelp-sql/pom.xml
index 7237284c..f899b4b7 100644
--- a/kelp-sql/pom.xml
+++ b/kelp-sql/pom.xml
@@ -5,7 +5,7 @@
parent
com.github.pxav.kelp
- 0.3.1
+ 0.3.2
4.0.0
@@ -20,7 +20,7 @@
com.github.pxav.kelp
core
- 0.3.1
+ 0.3.2
provided
diff --git a/pom.xml b/pom.xml
index e1eb19c9..4a557119 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
com.github.pxav.kelp
parent
pom
- 0.3.1
+ 0.3.2
Kelp
A cross-version spigot framework to avoid boilerplate code and make your plugin compatible with multiple spigot versions easily
diff --git a/testing-module/pom.xml b/testing-module/pom.xml
index ec3f5d4a..cb9f8beb 100644
--- a/testing-module/pom.xml
+++ b/testing-module/pom.xml
@@ -5,7 +5,7 @@
parent
com.github.pxav.kelp
- 0.3.1
+ 0.3.2
4.0.0
@@ -45,7 +45,7 @@
com.github.pxav.kelp
core
- 0.3.1
+ 0.3.2
provided
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcCommand.java
new file mode 100644
index 00000000..736f8340
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcCommand.java
@@ -0,0 +1,45 @@
+package de.pxav.kelp.testing.npc;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import de.pxav.kelp.core.command.CreateCommand;
+import de.pxav.kelp.core.command.ExecutorType;
+import de.pxav.kelp.core.command.KelpCommand;
+import de.pxav.kelp.core.npc.KelpNpc;
+import de.pxav.kelp.core.player.KelpPlayer;
+
+import javax.inject.Singleton;
+import java.util.UUID;
+
+@Singleton
+@CreateCommand(
+ name = "knpc",
+ executorType = ExecutorType.PLAYER_ONLY)
+public class NpcCommand extends KelpCommand {
+
+ private static Multimap playerNpcs = HashMultimap.create();
+
+ @Override
+ public void onCommandRegister() {
+ permission("kelp.test.npc");
+ noPlayerMessage("§cYou have to be a player to use this command");
+ }
+
+ @Override
+ public void onCommand(KelpPlayer player, String[] args) {
+ if (args.length == 0) {
+ player.sendPrefixedMessages("§8[§2Kelp§8] ",
+ "§8§m----------------------------------",
+ "§7",
+ "§7/knpc spawn",
+ "§7/knpc removeAll",
+ "",
+ "§8§m----------------------------------");
+ }
+ }
+
+ public static Multimap getPlayerNpcs() {
+ return playerNpcs;
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcRemoveAllCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcRemoveAllCommand.java
new file mode 100644
index 00000000..70702235
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcRemoveAllCommand.java
@@ -0,0 +1,28 @@
+package de.pxav.kelp.testing.npc;
+
+import de.pxav.kelp.core.command.CreateSubCommand;
+import de.pxav.kelp.core.command.ExecutorType;
+import de.pxav.kelp.core.command.KelpCommand;
+import de.pxav.kelp.core.command.KelpConsoleSender;
+import de.pxav.kelp.core.npc.KelpNpc;
+
+@CreateSubCommand(name = "removeAll",
+ executorType = ExecutorType.PLAYER_AND_CONSOLE,
+ parentCommand = NpcCommand.class)
+public class NpcRemoveAllCommand extends KelpCommand {
+
+ @Override
+ public void onCommandRegister() {
+ delegatePlayerToConsole(true);
+ inheritFromMainCommand(true);
+ }
+
+ @Override
+ public void onCommand(KelpConsoleSender consoleSender, String[] args) {
+ NpcCommand.getPlayerNpcs().keySet().forEach(uuid
+ -> NpcCommand.getPlayerNpcs().get(uuid).forEach(KelpNpc::remove));
+ NpcCommand.getPlayerNpcs().clear();
+ consoleSender.sendMessage("§8[§2Kelp§8] §7All NPCs have been removed successfully.");
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcSpawnCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcSpawnCommand.java
new file mode 100644
index 00000000..7fa25bf5
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/npc/NpcSpawnCommand.java
@@ -0,0 +1,57 @@
+package de.pxav.kelp.testing.npc;
+
+
+
+import de.pxav.kelp.core.command.CreateSubCommand;
+import de.pxav.kelp.core.command.ExecutorType;
+import de.pxav.kelp.core.command.KelpCommand;
+import de.pxav.kelp.core.event.kelpevent.npc.NpcInteractAction;
+import de.pxav.kelp.core.npc.KelpNpc;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.scheduler.type.DelayedScheduler;
+import de.pxav.kelp.testing.npc.gui.NpcGui;
+
+import javax.inject.Inject;
+
+@CreateSubCommand(
+ name = "spawn",
+ executorType = ExecutorType.PLAYER_ONLY,
+ parentCommand = NpcCommand.class)
+public class NpcSpawnCommand extends KelpCommand {
+
+ @Inject private NpcGui gui;
+
+ @Override
+ public void onCommandRegister() {
+ inheritFromMainCommand(true);
+ }
+
+ @Override
+ public void onCommand(KelpPlayer player, String[] args) {
+ KelpNpc npc = KelpNpc.create();
+
+ npc.location(player.getLocation());
+ npc.player(player);
+ npc.customName("§2Kelp Demo NPC");
+ npc.showCustomName();
+ npc.onInteract(event -> {
+ if (event.getAction() == NpcInteractAction.RIGHT_CLICK) {
+ DelayedScheduler.create().withDelayOf(50).milliseconds().run(taskId -> {
+ gui.open(npc);
+ });
+
+ return;
+ }
+
+ // todo if npc is attackable, play damage animation
+ });
+
+ npc.spawn();
+ npc.lookTo(player.getLocation());
+
+ NpcCommand.getPlayerNpcs().put(player.getUUID(), npc);
+ player.sendMessage("§8[§2Kelp§8] §7An NPC has been spawned at your location.");
+ player.sendMessage("§8[§2Kelp§8] §7Right click it to change some settings.");
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/npc/gui/NpcGui.java b/testing-module/src/main/java/de/pxav/kelp/testing/npc/gui/NpcGui.java
new file mode 100644
index 00000000..cabbaf92
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/npc/gui/NpcGui.java
@@ -0,0 +1,107 @@
+package de.pxav.kelp.testing.npc.gui;
+
+import de.pxav.kelp.core.inventory.item.KelpItem;
+import de.pxav.kelp.core.inventory.material.KelpMaterial;
+import de.pxav.kelp.core.inventory.type.AnimatedInventory;
+import de.pxav.kelp.core.inventory.widget.ItemWidget;
+import de.pxav.kelp.core.inventory.widget.Pagination;
+import de.pxav.kelp.core.npc.KelpNpc;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.player.prompt.PromptResponseType;
+import de.pxav.kelp.core.scheduler.type.DelayedScheduler;
+import org.bukkit.ChatColor;
+
+import java.util.concurrent.TimeUnit;
+
+public class NpcGui {
+
+ private static final String descriptionLine = "§8§m----------------------------";
+
+ public void open(KelpNpc npc) {
+ KelpPlayer player = npc.getPlayer();
+ AnimatedInventory inventory = AnimatedInventory.create();
+
+ inventory.rows(4);
+
+ inventory.addWidget(ItemWidget.create()
+ .player(player)
+ .item(KelpItem.create()
+ .displayName("§6§lEDITING NPC")
+ .material(KelpMaterial.OAK_SIGN_ITEM)
+ .slot(4)
+ .addItemDescription(
+ "§8§m----------------------------",
+ "§7Name§8: §e" + npc.getCustomName(),
+ "§7Tab-Name§8: §e" + npc.getTabListName()
+ )));
+
+ inventory.addWidget(Pagination.create()
+ .player(player)
+ .contentSlots(
+ 10, 11, 12, 13, 14, 15, 16,
+ 19, 20, 21, 22, 23, 24, 25)
+ .contentItems(
+ KelpItem.create()
+ .displayName("§eEdit Custom Name")
+ .material(KelpMaterial.NAME_TAG)
+ .addItemDescription(
+ descriptionLine,
+ "§7Change the NPC's custom name")
+ .addListener(player, event -> {
+ player.closeInventory();
+ player.openAnvilPrompt()
+ .initialText(npc.getCustomName().replace("§", "&"))
+ .sourceMaterial(KelpMaterial.NAME_TAG)
+ .withAsyncTimeout(60, TimeUnit.SECONDS, () -> {}, true)
+ .handle(response -> {
+ if (response.length() >= 16) {
+ player.sendMessage("§cGiven name is too long. Limit is 16 chars.");
+ return PromptResponseType.TRY_AGAIN;
+ }
+
+ npc.customName(ChatColor.translateAlternateColorCodes('&', response));
+ player.sendMessage("§aCustom name has been changed successfully.");
+ return PromptResponseType.ACCEPTED;
+ });
+ }),
+ KelpItem.create()
+ .displayName("§cNPC is attackable")
+ .material(KelpMaterial.IRON_SWORD)
+ .addItemDescription(
+ descriptionLine,
+ "§7Toggle whether the NPC is attackable or not.",
+ "§c(coming soon)"
+ ),
+ KelpItem.create()
+ .displayName("§cChange title lines")
+ .material(KelpMaterial.PAINTING)
+ .addItemDescription(
+ descriptionLine,
+ "§7Change the NPC's text lines above its head"
+ ),
+ KelpItem.create()
+ .displayName("§eMake the NPC walk")
+ .material(KelpMaterial.RAIL)
+ .addItemDescription(
+ descriptionLine,
+ "§7Let the npc walk to a given target",
+ "§7or direction. No physics are implemented",
+ "§7yet."
+ )
+ )
+ .previousButton(KelpItem.create()
+ .displayName("§6§lPrevious page")
+ .slot(27)
+ .material(KelpMaterial.ARROW),
+ () -> {}) // do nothing when on first page
+ .nextButton(KelpItem.create()
+ .displayName("§6§lNext page")
+ .slot(35)
+ .material(KelpMaterial.ARROW),
+ () -> {}) // do nothing when on last page
+ );
+
+ player.openInventory(inventory);
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/npc/gui/TitleLineGui.java b/testing-module/src/main/java/de/pxav/kelp/testing/npc/gui/TitleLineGui.java
new file mode 100644
index 00000000..ed185083
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/npc/gui/TitleLineGui.java
@@ -0,0 +1,11 @@
+package de.pxav.kelp.testing.npc.gui;
+
+import de.pxav.kelp.core.npc.KelpNpc;
+
+public class TitleLineGui {
+
+ public void openTitleLineEditor(KelpNpc npc) {
+
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/region/CreateRegionCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/region/CreateRegionCommand.java
new file mode 100644
index 00000000..ad7b6f96
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/region/CreateRegionCommand.java
@@ -0,0 +1,27 @@
+package de.pxav.kelp.testing.region;
+
+import de.pxav.kelp.core.command.CreateSubCommand;
+import de.pxav.kelp.core.command.ExecutorType;
+import de.pxav.kelp.core.command.KelpCommand;
+import de.pxav.kelp.core.player.KelpPlayer;
+
+import javax.inject.Singleton;
+
+@Singleton
+@CreateSubCommand(name = "create", executorType = ExecutorType.PLAYER_ONLY, parentCommand = KRegionCommand.class)
+public class CreateRegionCommand extends KelpCommand {
+
+ @Override
+ public void onCommandRegister() {
+ argumentsStartFromZero(true);
+ allowCustomParameters(true);
+ }
+
+ @Override
+ public void onCommand(KelpPlayer player, String[] args) {
+ if (args.length == 0) {
+ player.sendMessage("§8[§2§8] §7Please select either §aCUBOID §7or §aELLIPSOID");
+ }
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/region/KRegionCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/region/KRegionCommand.java
new file mode 100644
index 00000000..2a44ad7c
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/region/KRegionCommand.java
@@ -0,0 +1,34 @@
+package de.pxav.kelp.testing.region;
+
+import de.pxav.kelp.core.command.CreateCommand;
+import de.pxav.kelp.core.command.ExecutorType;
+import de.pxav.kelp.core.command.KelpCommand;
+import de.pxav.kelp.core.player.KelpPlayer;
+
+import javax.inject.Singleton;
+
+@Singleton
+@CreateCommand(name = "kregion", executorType = ExecutorType.PLAYER_ONLY)
+public class KRegionCommand extends KelpCommand {
+
+ @Override
+ public void onCommandRegister() {
+
+ }
+
+ @Override
+ public void onCommand(KelpPlayer player, String[] args) {
+ if (args.length == 0) {
+ player.sendPrefixedMessages("§8[§2Kelp§8] ",
+ "§8§m----------------------------------",
+ "§7",
+ "§cWork in progress",
+ "§7/kregion wand",
+ "§7/kregion create ",
+ "§7/kregion edit",
+ "",
+ "§8§m----------------------------------");
+ }
+ }
+
+}
diff --git a/testing-module/src/main/java/de/pxav/kelp/testing/region/WandCommand.java b/testing-module/src/main/java/de/pxav/kelp/testing/region/WandCommand.java
new file mode 100644
index 00000000..aacdb6c3
--- /dev/null
+++ b/testing-module/src/main/java/de/pxav/kelp/testing/region/WandCommand.java
@@ -0,0 +1,22 @@
+package de.pxav.kelp.testing.region;
+
+import de.pxav.kelp.core.command.CreateSubCommand;
+import de.pxav.kelp.core.command.ExecutorType;
+import de.pxav.kelp.core.command.KelpCommand;
+import de.pxav.kelp.core.player.KelpPlayer;
+
+import javax.inject.Singleton;
+
+@Singleton
+@CreateSubCommand(name = "wand", executorType = ExecutorType.PLAYER_ONLY, parentCommand = KRegionCommand.class)
+public class WandCommand extends KelpCommand {
+
+ @Override
+ public void onCommandRegister() {
+ super.onCommandRegister();
+ }
+
+ @Override
+ public void onCommand(KelpPlayer player, String[] args) {}
+
+}
diff --git a/v1_8_implementation/pom.xml b/v1_8_implementation/pom.xml
index 9d14af90..bc735fa8 100644
--- a/v1_8_implementation/pom.xml
+++ b/v1_8_implementation/pom.xml
@@ -5,7 +5,7 @@
com.github.pxav.kelp
parent
- 0.3.1
+ 0.3.2
4.0.0
@@ -90,7 +90,7 @@
com.github.pxav.kelp
core
- 0.3.1
+ 0.3.2
provided
diff --git a/v1_8_implementation/src/main/java/de/pxav/kelp/implementation1_8/world/VersionedBlock.java b/v1_8_implementation/src/main/java/de/pxav/kelp/implementation1_8/world/VersionedBlock.java
index bce35e79..13acd87d 100644
--- a/v1_8_implementation/src/main/java/de/pxav/kelp/implementation1_8/world/VersionedBlock.java
+++ b/v1_8_implementation/src/main/java/de/pxav/kelp/implementation1_8/world/VersionedBlock.java
@@ -6,6 +6,7 @@
import de.pxav.kelp.core.inventory.material.MaterialContainer;
import de.pxav.kelp.core.version.Versioned;
import de.pxav.kelp.core.world.KelpLocation;
+import de.pxav.kelp.core.world.util.KelpBlockFace;
import de.pxav.kelp.core.world.version.BlockVersionTemplate;
import de.pxav.kelp.core.world.KelpBlock;
import de.pxav.kelp.core.world.KelpChunk;
@@ -118,7 +119,7 @@ public boolean canApplyBoneMeal(KelpBlock block) {
* @param blockFace The face of the block to apply the bone meal to.
*/
@Override
- public void applyBoneMeal(KelpBlock kBlock, BlockFace blockFace) {
+ public void applyBoneMeal(KelpBlock kBlock, KelpBlockFace blockFace) {
Block block = kBlock.getBukkitBlock();
// cause those plant types to grow by 2 to 5 stages.
diff --git a/v_1_14_implementation/pom.xml b/v_1_14_implementation/pom.xml
index a18ea919..649854f7 100644
--- a/v_1_14_implementation/pom.xml
+++ b/v_1_14_implementation/pom.xml
@@ -5,7 +5,7 @@
parent
com.github.pxav.kelp
- 0.3.1
+ 0.3.2
4.0.0
@@ -57,7 +57,7 @@
com.github.pxav.kelp
core
- 0.3.1
+ 0.3.2
org.spigotmc