/*
* 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.internal.regions;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
import com.jcwhatever.nucleus.Nucleus;
import com.jcwhatever.nucleus.collections.players.PlayerMap;
import com.jcwhatever.nucleus.internal.regions.PlayerLocationCache.CachedLocation;
import com.jcwhatever.nucleus.managed.scheduler.Scheduler;
import com.jcwhatever.nucleus.providers.npc.Npcs;
import com.jcwhatever.nucleus.regions.IRegion;
import com.jcwhatever.nucleus.regions.IRegionEventListener;
import com.jcwhatever.nucleus.regions.options.LeaveRegionReason;
import com.jcwhatever.nucleus.regions.options.RegionEventPriority.PriorityType;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.coords.LocationUtils;
import com.jcwhatever.nucleus.utils.performance.pool.IPoolElementFactory;
import com.jcwhatever.nucleus.utils.performance.pool.SimpleCheckoutPool.CheckedOutElements;
import com.jcwhatever.nucleus.utils.performance.pool.SimplePool;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
/**
* Watches and tracks players for the purpose of detecting
* entry and exit from event listening regions.
*/
public final class InternalPlayerWatcher {
private final InternalRegionManager _manager;
// cached regions the player was detected in during last player watcher cycle.
private final PlayerMap<EventOrderedRegions<IRegion>> _playerRegionCache;
// locations the player was detected in between player watcher cycles.
private final PlayerMap<PlayerLocationCache> _playerLocationCache;
// async player watcher
private final PlayerWatcherAsync _watcherAsync = new PlayerWatcherAsync();
// sync region event caller
private final EventCaller _eventCaller = new EventCaller();
// IDs of players that have joined the server within a region and have not yet moved.
private Set<UUID> _joined = new HashSet<>(10);
// pool of player location caches
private final SimplePool<PlayerLocationCache> _pools;
private volatile boolean _isAsyncWatcherRunning;
/**
* Constructor.
*
* @param manager The parent manager.
*/
InternalPlayerWatcher(InternalRegionManager manager) {
_manager = manager;
_playerRegionCache = new PlayerMap<>(Nucleus.getPlugin());
_playerLocationCache = new PlayerMap<>(Nucleus.getPlugin());
_pools = new SimplePool<PlayerLocationCache>(100,
new IPoolElementFactory<PlayerLocationCache>() {
@Override
public PlayerLocationCache create() {
return new PlayerLocationCache();
}
});
Scheduler.runTaskRepeat(Nucleus.getPlugin(), 1, 1, new QueueFiller());
Scheduler.runTaskRepeatAsync(Nucleus.getPlugin(), 1, 1, _watcherAsync);
}
/**
* Add a location that the player has moved to so it can be
* cached and processed by the player watcher the next time it
* runs.
*
* <p>The players current location is used.</p>
*
* @param player The player.
* @param reason The reason that will be used if the player enters a region.
*/
public void updatePlayerLocation(Player player, RegionEventReason reason) {
PreCon.notNull(player);
PreCon.notNull(reason);
if (player.isDead() && reason != RegionEventReason.RESPAWN)
return;
// ignore NPC's
if (Npcs.isNpc(player))
return;
if (!_manager.getListenerWorlds().contains(player.getWorld()))
return;
PlayerLocationCache locations = getPlayerLocations(player.getUniqueId());
player.getLocation(locations.add(reason));
}
/**
* Add a location that the player has moved to so it can be
* cached and processed by the player watcher the next time it
* runs.
*
* @param player The player.
* @param location The location to add.
* @param reason The reason that will be used if the player enters a region.
*/
public void updatePlayerLocation(Player player, Location location, RegionEventReason reason) {
PreCon.notNull(player);
PreCon.notNull(reason);
if (player.isDead() && reason != RegionEventReason.RESPAWN)
return;
// ignore NPC's
if (Npcs.isNpc(player))
return;
if (!_manager.getListenerWorlds().contains(player.getWorld()))
return;
PlayerLocationCache locations = getPlayerLocations(player.getUniqueId());
LocationUtils.copy(location, locations.add(reason));
}
/**
* Update player location when the player does not have a location.
*
* <p>Declares the player as leaving all current regions.</p>
*
* @param player The player.
* @param reason The reason the player is leaving the regions.
*/
public void updatePlayerLocation(Player player, LeaveRegionReason reason) {
PreCon.notNull(player);
PreCon.notNull(reason);
// ignore NPC's
if (Npcs.isNpc(player))
return;
UUID playerId = player.getUniqueId();
EventOrderedRegions<IRegion> regions = forgetPlayer(playerId);
if (regions == null)
return;
synchronized (this) {
Iterator<IRegion> iterator = regions.iterator(PriorityType.LEAVE);
while (iterator.hasNext()) {
IRegion region = iterator.next();
if (region.isEventListener())
region.getEventListener().onPlayerLeave(player, reason);
}
}
if (reason == LeaveRegionReason.QUIT_SERVER) {
synchronized (this) {
// re-pool location cache
PlayerLocationCache cache = _playerLocationCache.remove(player.getUniqueId());
if (cache != null) {
_pools.recycle(cache);
}
}
}
else {
clearPlayerLocations(player.getUniqueId());
}
}
/**
* Get the cached movement locations of a player that have not been processed
* by the {@link InternalPlayerWatcher} yet.
*/
PlayerLocationCache getPlayerLocations(UUID playerId) {
PlayerLocationCache locations = _playerLocationCache.get(playerId);
if (locations == null) {
locations = _pools.retrieve();
assert locations != null;
locations.setOwner(playerId);
_playerLocationCache.put(playerId, locations);
}
return locations;
}
/**
* Get the regions a player is currently in.
*
* <p>The {@link InternalPlayerWatcher} should be synchronized when using the returned
* {@link EventOrderedRegions}.</p>
*
* @param playerId The ID of the player to check.
*/
@Nullable
EventOrderedRegions<IRegion> getCurrentRegions(UUID playerId) {
synchronized (this) {
return _playerRegionCache.get(playerId);
}
}
/**
* Remove the regions a player is currently in from the cache.
*
* @param playerId The ID of the player to check.
*
* @return The removed regions.
*/
@Nullable
EventOrderedRegions<IRegion> forgetPlayer(UUID playerId) {
synchronized (this) {
return _playerRegionCache.remove(playerId);
}
}
/*
* Clear cached movement locations of a player
*/
private void clearPlayerLocations(UUID playerId) {
PlayerLocationCache locations = getPlayerLocations(playerId);
locations.getCheckedOut().recycle();
}
/*
* Repeating task that pre-processes and filters player movement data before
* adding it to the async player watchers queue.
*/
private final class QueueFiller implements Runnable {
@Override
public void run() {
// do not run while async watcher is running
if (_isAsyncWatcherRunning)
return;
// call queued region events
if (!_eventCaller.queue.isEmpty()) {
_eventCaller.run();
}
// get worlds where listener regions exist
List<World> worlds = new ArrayList<World>(_manager.getListenerWorlds().getElements());
// get players in worlds with regions
for (World world : worlds) {
if (world == null)
continue;
// get players currently in world
List<Player> players = world.getPlayers();
if (players == null || players.isEmpty())
continue;
// process and add players to wp list
for (Player player : players) {
// do not process NPC's
if (Npcs.isNpc(player))
continue;
// get locations that the player was recorded in between watcher cycles
PlayerLocationCache locations = getPlayerLocations(player.getUniqueId());
synchronized (InternalPlayerWatcher.this) {
// skip if there are no locations recorded
if (locations.getCheckedOut().size() == 0)
continue;
WorldPlayer worldPlayer = new WorldPlayer(player, locations.getCheckedOut());
_watcherAsync.queue.add(worldPlayer);
}
}
}
// end if there are no players to process
if (_watcherAsync.queue.isEmpty())
return;
// let the async watcher run
_isAsyncWatcherRunning = true;
}
}
/*
* Async portion of the player watcher
*/
private class PlayerWatcherAsync implements Runnable {
final Queue<WorldPlayer> queue = new ArrayDeque<>(Bukkit.getMaxPlayers());
// Temporary queue to hold regions a player has possibly entered.
// Because "enter" is processed before "leave", the new "enter" event
// will not be processed unless regions in question are processed after
// the "leave" event has removed the region from the players region cache.
// The enter queue is used to hold regions that need to be checked again
// after "leave" events have been processed.
final Queue<IRegion> enter = new ArrayDeque<>(20);
@Override
public void run() {
if (!_isAsyncWatcherRunning)
return;
// iterate players
while (!queue.isEmpty()) {
WorldPlayer worldPlayer = queue.remove();
UUID playerId = worldPlayer.player.getUniqueId();
// get regions the player is in (cached from previous check)
EventOrderedRegions<IRegion> cachedRegions;
synchronized (InternalPlayerWatcher.this) {
cachedRegions = _playerRegionCache.get(playerId);
}
if (cachedRegions == null) {
cachedRegions = new EventOrderedRegions<>(7);
synchronized (InternalPlayerWatcher.this) {
_playerRegionCache.put(playerId, cachedRegions);
}
}
// iterate locations the player has been since the last check
for (CachedLocation location : worldPlayer.locations) {
if (location.getReason() == RegionEventReason.JOIN_SERVER) {
// do not notify regions until player has moved.
// Allows time for player to load resource packs.
_joined.add(playerId);
continue;
}
boolean isJoining = _joined.contains(playerId);
if (isJoining && location.getReason() != RegionEventReason.MOVE) {
// ignore all other reasons until joined player moves
continue;
}
// get regions the player location is in
List<IRegion> locationRegions = _manager.getListenerRegions(location, PriorityType.ENTER);
RegionEventReason reason = null;
// check ENTER regions
if (!locationRegions.isEmpty()) {
reason = isJoining && _joined.remove(playerId)
? RegionEventReason.JOIN_SERVER
: location.getReason();
for (IRegion region : locationRegions) {
synchronized (InternalPlayerWatcher.this) {
// check if player was not previously in region
if (cachedRegions.contains(region)) {
// add to "enter" queue to verify later
enter.offer(region);
} else {
cachedRegions.add(region);
_eventCaller.queue.addLast(new EventInfo(
region, worldPlayer.player, reason, true
));
}
}
}
}
// check LEAVE regions
synchronized (InternalPlayerWatcher.this) {
if (!cachedRegions.isEmpty()) {
// get iterator for regions the player is currently in.
Iterator<IRegion> iterator = cachedRegions.iterator(PriorityType.LEAVE);
while (iterator.hasNext()) {
IRegion region = iterator.next();
// check if player was previously in region
if (!locationRegions.contains(region) || reason == RegionEventReason.DEAD) {
//remove from players cached regions
iterator.remove();
_eventCaller.queue.addLast(new EventInfo(
region, worldPlayer.player, location.getReason(), false
));
}
}
}
}
// verify "enter" queue
while (reason != null && !enter.isEmpty()) {
IRegion region = enter.poll();
synchronized (InternalPlayerWatcher.this) {
// check if player was not previously in region
if (!cachedRegions.contains(region)) {
cachedRegions.add(region);
_eventCaller.queue.addLast(new EventInfo(
region, worldPlayer.player, reason, true
));
}
}
}
}
// recycle player locations so they can be reused
worldPlayer.locations.recycle();
} // END while(queue.isEmpty)
_isAsyncWatcherRunning = false;
} // END run()
}
/**
* Represents a player and the locations they have been
* since the last player watcher cycle.
*/
private static class WorldPlayer {
final Player player;
final CheckedOutElements<CachedLocation> locations;
public WorldPlayer(Player player, CheckedOutElements<CachedLocation> locations) {
this.player = player;
this.locations = locations;
}
}
/**
* Contains info for a region event that needs to be called
*/
private static class EventInfo {
final IRegion region;
final Player player;
final RegionEventReason reason;
final boolean isEntering;
EventInfo(IRegion region, Player player, RegionEventReason reason, boolean isEntering) {
this.region = region;
this.player = player;
this.reason = reason;
this.isEntering = isEntering;
}
}
/**
* Calls region events in queue on the main thread.
*/
private static class EventCaller implements Runnable {
final Deque<EventInfo> queue = new ArrayDeque<EventInfo>(Bukkit.getMaxPlayers());
@Override
public void run() {
while (!queue.isEmpty()) {
EventInfo info = queue.removeFirst();
if (info.region.isDisposed())
continue;
IRegionEventListener listener;
try {
listener = info.region.getEventListener();
if (listener == null) {
// print null pointer exception message to console.
throw new NullPointerException("Region event listener cannot be null.");
}
} catch (Throwable e) {
e.printStackTrace();
continue;
}
try {
if (info.isEntering)
listener.onPlayerEnter(info.player, info.reason.getEnterReason());
else
listener.onPlayerLeave(info.player, info.reason.getLeaveReason());
}
catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}