/*
* This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT).
*
* Copyright (c) JCThePants (www.jcwhatever.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.jcwhatever.nucleus.regions;
import com.jcwhatever.nucleus.Nucleus;
import com.jcwhatever.nucleus.events.regions.RegionOwnerChangedEvent;
import com.jcwhatever.nucleus.internal.regions.InternalRegionManager;
import com.jcwhatever.nucleus.regions.options.EnterRegionReason;
import com.jcwhatever.nucleus.regions.options.LeaveRegionReason;
import com.jcwhatever.nucleus.regions.options.RegionEventPriority;
import com.jcwhatever.nucleus.regions.options.RegionEventPriority.PriorityType;
import com.jcwhatever.nucleus.storage.IDataNode;
import com.jcwhatever.nucleus.utils.MetaStore;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.coords.IChunkCoords;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.plugin.Plugin;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
/**
* Abstract implementation of a region.
*
* <p>The region is registered with NucleusFramework's {@link IGlobalRegionManager} as
* soon as it is defined (P1 and P2 coordinates set) via the regions settings or by
* invoking the {@link #setCoords} method.</p>
*
* <p>The regions protected methods {@link #onPlayerEnter} and {@link #onPlayerLeave}
* are only invoked by the global region manager if the implementing type invokes
* {@link #setEventListener(boolean)} with a 'true' argument.</p>
*/
public abstract class Region extends SimpleRegionSelection implements IRegion {
private static final Map<Region, Void> _instances = new WeakHashMap<>(100);
private static BukkitListener _bukkitListener;
private final Plugin _plugin;
private final String _name;
private final String _searchName;
private final IDataNode _dataNode;
private final IRegionEventListener _eventListener;
private final MetaStore _meta = new MetaStore();
private RegionEventPriority _enterPriority = RegionEventPriority.DEFAULT;
private RegionEventPriority _leavePriority = RegionEventPriority.DEFAULT;
private boolean _isEventListener;
private boolean _isDisposed;
private UUID _ownerId;
private int _priority = 0;
private List<IRegionEventHandler> _eventHandlers = new ArrayList<>(10);
/**
* Constructor
*/
public Region(Plugin plugin, String name, @Nullable IDataNode dataNode) {
PreCon.notNull(plugin);
PreCon.notNullOrEmpty(name);
_meta.setKey(InternalRegionManager.REGION_HANDLE, this);
_eventListener = new RegionListener();
_name = name;
_plugin = plugin;
_searchName = name.toLowerCase();
_dataNode = dataNode;
RegionPriorityInfo info = this.getClass().getAnnotation(RegionPriorityInfo.class);
if (info != null) {
_enterPriority = info.enter();
_leavePriority = info.leave();
}
if (dataNode != null) {
loadSettings(dataNode);
}
_instances.put(this, null);
if (_bukkitListener == null) {
_bukkitListener = new BukkitListener();
Bukkit.getPluginManager().registerEvents(_bukkitListener, Nucleus.getPlugin());
}
}
@Override
public final String getName() {
return _name;
}
@Override
public final String getSearchName() {
return _searchName;
}
@Override
public final Plugin getPlugin() {
return _plugin;
}
@Override
public int getPriority() {
return _priority;
}
/**
* Set the regions priority.
*
* @param priority The priority value. Higher numbers have higher priority.
*/
protected void setPriority(int priority) {
if (_priority == priority)
return;
_priority = priority;
IDataNode dataNode = getDataNode();
if (dataNode != null) {
dataNode.set("priority", priority);
dataNode.save();
}
regionManager().register(this);
}
@Override
public RegionEventPriority getEventPriority(PriorityType priorityType) {
PreCon.notNull(priorityType);
switch (priorityType) {
case ENTER:
return _enterPriority;
case LEAVE:
return _leavePriority;
default:
throw new AssertionError();
}
}
@Override
public final boolean isEventListener() {
return _isEventListener || !_eventHandlers.isEmpty();
}
@Override
public final IRegionEventListener getEventListener() {
return _eventListener;
}
@Override
@Nullable
public UUID getOwnerId() {
return _ownerId;
}
@Override
public boolean hasOwner() {
return _ownerId != null;
}
@Override
public boolean setOwner(@Nullable UUID ownerId) {
UUID oldId = getOwnerId();
RegionOwnerChangedEvent event = new RegionOwnerChangedEvent(
new ReadOnlyRegion(this), oldId, ownerId);
Nucleus.getEventManager().callBukkit(this, event);
if (event.isCancelled())
return false;
if (!onOwnerChanged(oldId, ownerId)) {
return false;
}
_ownerId = ownerId;
IDataNode dataNode = getDataNode();
if (dataNode != null) {
dataNode.set("owner-id", ownerId);
dataNode.save();
}
return true;
}
@Override
public final void setCoords(Location p1, Location p2) {
PreCon.notNull(p1);
PreCon.notNull(p2);
// unregister while math is updated,
// is re-registered after math update (see UpdateMath)
regionManager().unregister(this);
super.setCoords(p1, p2);
IDataNode dataNode = getDataNode();
if (dataNode != null) {
dataNode.set("p1", getP1());
dataNode.set("p2", getP2());
dataNode.save();
}
onCoordsChanged(getP1(), getP2());
}
@Override
public MetaStore getMeta() {
return _meta;
}
@Override
public boolean addEventHandler(IRegionEventHandler handler) {
PreCon.notNull(handler);
boolean isFirstHandler = _eventHandlers.isEmpty();
if (_eventHandlers.add(handler)) {
if (isFirstHandler) {
// update registration
regionManager().register(this);
}
return true;
}
return false;
}
@Override
public boolean removeEventHandler(IRegionEventHandler handler) {
PreCon.notNull(handler);
if (_eventHandlers.remove(handler)) {
if (_eventHandlers.isEmpty()) {
// update registration
regionManager().register(this);
}
return true;
}
return false;
}
@Override
public final boolean contains(Material material) {
synchronized (getSync()) {
if (getWorld() == null)
return false;
int xlen = getXEnd();
int ylen = getYEnd();
int zlen = getZEnd();
for (int x = getXStart(); x <= xlen; x++) {
for (int y = getYStart(); y <= ylen; y++) {
for (int z = getZStart(); z <= zlen; z++) {
Block block = getWorld().getBlockAt(x, y, z);
if (block.getType() != material)
continue;
return true;
}
}
}
return false;
}
}
@Override
public final LinkedList<Location> find(Material material) {
return find(material, new LinkedList<Location>());
}
@Override
public final <T extends Collection<Location>> T find(Material material, T output) {
PreCon.notNull(material);
PreCon.notNull(output);
synchronized (getSync()) {
if (getWorld() == null)
return output;
int xlen = getXEnd();
int ylen = getYEnd();
int zlen = getZEnd();
for (int x = getXStart(); x <= xlen; x++) {
for (int y = getYStart(); y <= ylen; y++) {
for (int z = getZStart(); z <= zlen; z++) {
Block block = getWorld().getBlockAt(x, y, z);
if (block.getType() != material)
continue;
output.add(block.getLocation());
}
}
}
return output;
}
}
@Override
public final void refreshChunks() {
World world = getWorld();
if (world == null)
return;
Collection<IChunkCoords> chunks = getChunkCoords();
for (IChunkCoords chunk : chunks) {
world.refreshChunk(chunk.getX(), chunk.getZ());
}
}
@Override
public final void removeEntities(Class<?>... entityTypes) {
synchronized (getSync()) {
Collection<IChunkCoords> chunks = getChunkCoords();
for (IChunkCoords chunkInfo : chunks) {
Chunk chunk = chunkInfo.getChunk();
if (chunk == null)
return;
for (Entity entity : chunk.getEntities()) {
if (contains(entity.getLocation())) {
if (entityTypes == null || entityTypes.length == 0) {
entity.remove();
continue;
}
for (Class<?> itemType : entityTypes) {
if (!itemType.isInstance(entity))
continue;
entity.remove();
break;
}
}
}
}
}
}
@Override
public Class<? extends IRegion> getRegionClass() {
return getClass();
}
@Override
public final boolean isDisposed() {
return _isDisposed;
}
@Override
public final void dispose() {
regionManager().unregister(this);
_instances.remove(this);
onDispose();
_isDisposed = true;
}
@Override
public int hashCode() {
return _name.hashCode();
}
@Override
public boolean equals(Object obj) {
synchronized (getSync()) {
return this == obj;
}
}
/**
* Get the regions data node.
*/
@Nullable
protected IDataNode getDataNode() {
return _dataNode;
}
/**
* Set the value of the event watcher flag and update the regions registration
* with the global region manager.
*
* @param isEventListener True to allow player enter and leave events. If entry/exit events
* are not used, false.
*/
protected void setEventListener(boolean isEventListener) {
if (isEventListener != _isEventListener) {
_isEventListener = isEventListener;
regionManager().register(this);
}
}
/**
* Causes the onPlayerEnter method to re-fire if the player is already in the region.
*
* @param player The player to forget.
*/
protected void forgetPlayer(Player player) {
regionManager().forgetPlayer(player, this);
}
/**
* Initializes region coordinates without saving to the data node.
*
* @param p1 The first point location.
* @param p2 The second point location.
*/
protected final void initCoords(@Nullable Location p1, @Nullable Location p2) {
if (p1 != null && p2 != null) {
super.setCoords(p1, p2);
}
updateMath();
}
/**
* Initial load of settings from regions data node.
*
* @param dataNode The data node to load from
*/
protected void loadSettings(final IDataNode dataNode) {
Location p1 = dataNode.getLocation("p1");
Location p2 = dataNode.getLocation("p2");
initCoords(p1, p2);
_ownerId = dataNode.getUUID("owner-id");
_priority = dataNode.getInteger("priority");
_enterPriority = dataNode.getEnum(
"region-enter-priority", _enterPriority, RegionEventPriority.class);
_leavePriority = dataNode.getEnum(
"region-leave-priority", _leavePriority, RegionEventPriority.class);
}
/*
* Update region math variables.
*/
@Override
protected void updateMath() {
super.updateMath();
regionManager().register(this);
}
/**
* Invoked when the coordinates for the region are changed.
*
* <p>Intended for optional override.</p>
*
* @param p1 The first point location.
* @param p2 The second point location.
*
* @throws IOException
*/
protected void onCoordsChanged(@Nullable Location p1, @Nullable Location p2) {
// do nothing
}
/**
* Invoked when a player enters the region, but only if the region is an event watcher and
* {@link #canDoPlayerEnter} returns true when invoked by the global region manager.
*
* <p>Intended for optional override.</p>
*
* @param player The player entering the region.
*/
protected void onPlayerEnter (Player player, EnterRegionReason reason) {
// do nothing
}
/**
* Invoked when a player leaves the region, but only if the region is an event watcher and
* {@link #canDoPlayerLeave} returns true when invoked by the global region manager.
*
* <p>Intended for optional override.</p>
*
* @param player The player leaving the region.
*/
protected void onPlayerLeave (Player player, LeaveRegionReason reason) {
// do nothing
}
/**
* Invoked to determine if {@link #onPlayerEnter} can be invoked on the specified player
* for the specified reason.
*
* <p>Normally returns true.</p>
*
* <p>Intended for optional override.</p>
*
* @param player The player entering the region.
*
* @return True to allow further handling. Normally returns true unless overridden.
*/
protected boolean canDoPlayerEnter(Player player, EnterRegionReason reason) {
return true;
}
/**
* Invoked to determine if {@link #onPlayerLeave} can be invoked on the specified player
* for the specified reason.
*
* <p>Intended for optional override.</p>
*
* @param player The player leaving the region.
*
* @return True to allow further handling. Normally returns true unless overridden.
*/
protected boolean canDoPlayerLeave(Player player, LeaveRegionReason reason) {
return true;
}
/**
* Invoked when the owner of the region is changed.
*
* <p>Note that other plugins can easily change the region
* owner value. If the owner is important, this should be used.</p>
*
* <p>Intended for optional override.</p>
*
* @param oldOwnerId The ID of the previous owner of the region.
* @param newOwnerId The ID of the new owner of the region.
*
* @return True to allow the owner change. Normally returns true unless overridden.
*/
protected boolean onOwnerChanged(@Nullable UUID oldOwnerId, @Nullable UUID newOwnerId) {
return true;
}
/**
* Invoked when the region is disposed.
*
* <p>Intended for optional override.</p>
*/
protected void onDispose() {
// do nothings
}
// helper method to get internal region manager.
private InternalRegionManager regionManager() {
return (InternalRegionManager) Nucleus.getRegionManager();
}
/*
* private implementation of IRegionEventListener
*/
private class RegionListener implements IRegionEventListener {
@Override
public void onPlayerEnter(Player player, EnterRegionReason reason) {
if (Region.this.canDoPlayerEnter(player, reason))
Region.this.onPlayerEnter(player, reason);
for (IRegionEventHandler handler : _eventHandlers) {
if (handler.canDoPlayerEnter(player, reason)) {
handler.onPlayerEnter(player, reason);
}
}
}
@Override
public void onPlayerLeave(Player player, LeaveRegionReason reason) {
if (Region.this.canDoPlayerLeave(player, reason))
Region.this.onPlayerLeave(player, reason);
for (IRegionEventHandler handler : _eventHandlers) {
if (handler.canDoPlayerLeave(player, reason)) {
handler.onPlayerLeave(player, reason);
}
}
}
}
/*
* Listens for world unload and load events and handles
* regions in the world appropriately.
*/
private static class BukkitListener implements Listener {
@EventHandler
private void onWorldLoad(WorldLoadEvent event) {
String worldName = event.getWorld().getName();
for (Region region : _instances.keySet()) {
if (region.isDefined() && worldName.equals(region.getWorldName())) {
// fix locations
//noinspection ConstantConditions
region.getP1().setWorld(event.getWorld());
//noinspection ConstantConditions
region.getP2().setWorld(event.getWorld());
}
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onWorldUnload(WorldUnloadEvent event) {
String worldName = event.getWorld().getName();
for (Region region : _instances.keySet()) {
if (region.isDefined() && worldName.equals(region.getWorldName())) {
// remove world from locations, helps garbage collector
//noinspection ConstantConditions
region.getP1().setWorld(null);
//noinspection ConstantConditions
region.getP2().setWorld(null);
}
}
}
}
}