Skip to content

Instantly share code, notes, and snippets.

@aadnk
Created October 8, 2012 18:21
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aadnk/3854038 to your computer and use it in GitHub Desktop.
Save aadnk/3854038 to your computer and use it in GitHub Desktop.
Entity proximity detection API
package com.comphenix.proximity;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.server.EntityPlayer;
import net.minecraft.server.EntityTracker;
import net.minecraft.server.EntityTrackerEntry;
import net.minecraft.server.WorldServer;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.craftbukkit.CraftWorld;
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.entity.CreatureSpawnEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.scheduler.BukkitScheduler;
import com.google.common.collect.ForwardingSet;
import com.google.common.collect.Maps;
public class EntityProximityDetector implements Listener {
// Number of ticks to wait
private static final int TASK_DELAY = 10;
// Used to revert the previous set
private Map<EntityTrackerEntry, Set<EntityPlayer>> notchSets = Maps.newHashMap();
private Map<Integer, EntityTrackerEntry> lookup = Maps.newHashMap();
// Entities to inject
private Deque<Entity> toInject = new ArrayDeque<Entity>();
private Plugin plugin;
private BukkitScheduler scheduler;
private PluginManager manager;
private int taskID = -1;
public EntityProximityDetector(Plugin plugin) {
this(plugin,
plugin.getServer().getScheduler(),
plugin.getServer().getPluginManager()
);
}
public EntityProximityDetector(Plugin plugin, BukkitScheduler scheduler, PluginManager manager) {
this.plugin = plugin;
this.scheduler = scheduler;
this.manager = manager;
}
/**
* Initialize the proximity detector.
* @param worlds - list of already loaded worlds.
*/
public void initialize(List<World> worlds) {
scheduleTask();
register(manager);
initializeWorlds(worlds);
}
/**
* Initialize the task that is responsible for injecting into the entity tracker.
*/
private void scheduleTask() {
if (taskID >= 0)
throw new IllegalStateException("Cannot schedule multiple tasks.");
// Schedule using the default delay (once per tick)
taskID = scheduler.scheduleSyncRepeatingTask(plugin, new Runnable() {
@Override
public void run() {
if (toInject.size() > 0) {
// Inject every scheduled entity
for (Iterator<Entity> it = toInject.descendingIterator(); it.hasNext(); ) {
// Remove successful injections
if (injectEntity(it.next()))
it.remove();
}
}
}
}, TASK_DELAY, TASK_DELAY);
// Might as well check this
if (taskID < 0) {
throw new IllegalStateException("Cannot schedule repeating task.");
}
}
/**
* Cancel a previously scheduled task.
*/
private void cancelTask() {
if (taskID >= 0) {
scheduler.cancelTask(taskID);
taskID = -1;
}
}
/**
* Register this proximity detector as an event listener.
* @param manager - plugin manager.
*/
private void register(PluginManager manager) {
manager.registerEvents(this, plugin);
}
/**
* Initialize based on already loaded chunks.
*/
private void initializeWorlds(List<World> worlds) {
for (World world : worlds) {
for (Chunk chunk : world.getLoadedChunks()) {
initializeChunk(chunk);
}
}
}
/**
* Initialize loaded chunks.
* @param chunk - the chunk with every loaded entity.
*/
private void initializeChunk(Chunk chunk) {
if (chunk == null)
return;
// Inject into every existing entity
for (Entity entity : chunk.getEntities()) {
if (entity != null)
toInject.addLast(entity);
}
}
/**
* Unload entities from chunk, if they have been injected before.
* @param chunk - the chunk with injected entities.
*/
private void unloadChunk(Chunk chunk) {
if (chunk == null)
return;
// Remove loaded entities
for (Entity entity : chunk.getEntities()) {
if (entity != null)
uninjectEntity(entity);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onChunkLoadEvent(ChunkLoadEvent event) {
initializeChunk(event.getChunk());
}
public void onChunkUnloadedEvent(ChunkUnloadEvent event) {
unloadChunk(event.getChunk());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerJoinedEvent(PlayerJoinEvent event) {
if (event.getPlayer() != null)
toInject.addLast(event.getPlayer());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onCreatureSpawnEvent(CreatureSpawnEvent event) {
if (event.getEntity() != null)
toInject.addLast(event.getEntity());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onEntityDeathEvent(CreatureSpawnEvent event) {
uninjectEntity(event.getEntity());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerQuitEvent(PlayerQuitEvent event) {
uninjectEntity(event.getPlayer());
}
// Invoked the first time an entity is nearby another player
protected void notifyAdding(Player observer, Entity visible) {
PlayerLoadEntityEvent loaded = new PlayerLoadEntityEvent(observer, visible);
manager.callEvent(loaded);
}
// Invoked the first time an entity leaves the vicinity of another player
protected void notifyRemoving(Player observer, Entity visible) {
PlayerUnloadEntityEvent unloaded = new PlayerUnloadEntityEvent(observer, visible);
manager.callEvent(unloaded);
}
protected void notifyAdding(Entity visible, Collection<?> source, Collection<?> target) {
for (Object element : source) {
if (target == null || (element instanceof EntityPlayer && !target.contains(element)))
notifyAdding(getBukkitPlayer(element), visible);
}
}
protected void notifyRemoving(Entity visible, Collection<?> source, Collection<?> target) {
for (Object element : source) {
if (target == null || (element instanceof EntityPlayer && target.contains(element)))
notifyRemoving(getBukkitPlayer(element), visible);
}
}
private Player getBukkitPlayer(Object entityPlayer) {
return ((EntityPlayer) entityPlayer).getBukkitEntity();
}
protected void uninjectEntity(Entity entity) {
int entityID = entity.getEntityId();
EntityTrackerEntry entry = lookup.get(entityID);
if (entry != null) {
uninjectEntity(entry);
// Clean up
lookup.remove(entityID);
notchSets.remove(entry);
}
}
private void uninjectEntity(EntityTrackerEntry entry) {
// Revert to the old tracker
if (entry != null) {
entry.trackedPlayers = notchSets.get(entry);
}
}
@SuppressWarnings("unchecked")
protected boolean injectEntity(final Entity visible) {
final World world = visible.getWorld();
final WorldServer worldServer = ((CraftWorld) world).getHandle();
final EntityTracker tracker = worldServer.tracker;
final EntityTrackerEntry entry = (EntityTrackerEntry) tracker.trackedEntities.
get(visible.getEntityId());
// Wait for the next tick if the entity isn't tracked yet
if (entry == null)
return false;
// Stop if another plugin has already injected into this set
else if (entry.trackedPlayers instanceof ForwardingSet)
return true;
final Set<EntityPlayer> notchSet = entry.trackedPlayers;
lookup.put(visible.getEntityId(), entry);
notchSets.put(entry, notchSet);
// Notify the already existing players
notifyAdding(visible, entry.trackedPlayers, null);
entry.trackedPlayers = new ForwardingSet<EntityPlayer>() {
@Override
protected Set<EntityPlayer> delegate() {
return notchSet;
}
@Override
public boolean add(EntityPlayer element) {
boolean success = super.add(element);
// Notify if this player was actually added
if (success)
notifyAdding(element.getBukkitEntity(), visible);
return success;
}
@Override
public boolean addAll(Collection<? extends EntityPlayer> collection) {
notifyAdding(visible, collection, this);
return super.addAll(collection);
}
@Override
public boolean remove(Object object) {
boolean success = super.remove(object);
if (object instanceof EntityPlayer && success)
notifyRemoving(getBukkitPlayer(object), visible);
return success;
}
@Override
public boolean removeAll(Collection<?> collection) {
notifyRemoving(visible, collection, this);
return super.removeAll(collection);
}
@Override
public void clear() {
notifyRemoving(visible, this, this);
super.clear();
}
};
// Successful injection
return true;
}
public void close() {
// Revert every set
for (EntityTrackerEntry entry : notchSets.keySet()) {
uninjectEntity(entry);
}
notchSets.clear();
lookup.clear();
cancelTask();
}
}
package com.comphenix.proximity;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
/**
* Invoked when a player is informed about the existence of a nearby entity.
*
* @author Kristian
*/
public class PlayerLoadEntityEvent extends PlayerEvent {
private static final HandlerList handlers = new HandlerList();
private Entity loadedEntity;
public PlayerLoadEntityEvent(Player who, Entity loadedEntity) {
super(who);
this.loadedEntity = loadedEntity;
}
/**
* Retrieve the loaded nearby entity.
* @return Nearby entity.
*/
public Entity getLoadedEntity() {
return loadedEntity;
}
/**
* This is a Bukkit method. Don't touch me.
*
* @return registered handlers to Bukkit
*/
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
// TODO Auto-generated method stub
return handlers;
}
}
package com.comphenix.proximity;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
/**
* Invoked when a player is no longer recieving information about a given entity.
*
* @author Kristian
*/
public class PlayerUnloadEntityEvent extends PlayerEvent {
private static final HandlerList handlers = new HandlerList();
private Entity unloadedEntity;
public PlayerUnloadEntityEvent(Player who, Entity unloadedEntity) {
super(who);
this.unloadedEntity = unloadedEntity;
}
/**
* Retrieve the unloaded entity.
* @return Entity nearby.
*/
public Entity getUnloadedEntity() {
return unloadedEntity;
}
/**
* This is a Bukkit method. Don't touch me.
*
* @return registered handlers to Bukkit
*/
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
// TODO Auto-generated method stub
return handlers;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment