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 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 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 multimap) { + for (Map.Entry 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 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 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 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 multimap) { + for (Map.Entry 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 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 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 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