package me.gtacraft; import static com.comphenix.protocol.PacketType.Play.Server.*; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Map; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDeathEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.plugin.Plugin; import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.events.PacketEvent; import com.google.common.base.Preconditions; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; public class EntityHider implements Listener { protected Table<Integer, Integer, Boolean> observerEntityMap = HashBasedTable.create(); // Packets that update remote player entities private static final PacketType[] ENTITY_PACKETS = { ENTITY_EQUIPMENT, BED, ANIMATION, NAMED_ENTITY_SPAWN, COLLECT, SPAWN_ENTITY, SPAWN_ENTITY_LIVING, SPAWN_ENTITY_PAINTING, SPAWN_ENTITY_EXPERIENCE_ORB, ENTITY_VELOCITY, REL_ENTITY_MOVE, ENTITY_LOOK, ENTITY_MOVE_LOOK, ENTITY_MOVE_LOOK, ENTITY_TELEPORT, ENTITY_HEAD_ROTATION, ENTITY_STATUS, ATTACH_ENTITY, ENTITY_METADATA, ENTITY_EFFECT, REMOVE_ENTITY_EFFECT, BLOCK_BREAK_ANIMATION // We don't handle DESTROY_ENTITY though }; /** * The current entity visibility policy. * @author Kristian */ public enum Policy { /** * All entities are invisible by default. Only entities specifically made visible may be seen. */ WHITELIST, /** * All entities are visible by default. An entity can only be hidden explicitly. */ BLACKLIST, } private ProtocolManager manager; // Listeners private Listener bukkitListener; private PacketAdapter protocolListener; // Current policy protected final Policy policy; /** * Construct a new entity hider. * @param plugin * @param policy */ public EntityHider(Plugin plugin, Policy policy) { Preconditions.checkNotNull(plugin, "plugin cannot be NULL."); // Save policy this.policy = policy; this.manager = ProtocolLibrary.getProtocolManager(); // Register events and packet listener plugin.getServer().getPluginManager().registerEvents( bukkitListener = constructBukkit(), plugin); manager.addPacketListener( protocolListener = constructProtocol(plugin)); } protected boolean setVisibility(Player observer, int entityID, boolean visible) { switch (policy) { case BLACKLIST: // Non-membership means they are visible return !setMembership(observer, entityID, !visible); case WHITELIST: return setMembership(observer, entityID, visible); default : throw new IllegalArgumentException("Unknown policy: " + policy); } } /** * Add or remove the given entity and observer entry from the table. * @param observer - the player observer. * @param entityID - ID of the entity. * @param member - TRUE if they should be present in the table, FALSE otherwise. * @return TRUE if they already were present, FALSE otherwise. */ // Helper method protected boolean setMembership(Player observer, int entityID, boolean member) { if (member) { return observerEntityMap.put(observer.getEntityId(), entityID, true) != null; } else { return observerEntityMap.remove(observer.getEntityId(), entityID) != null; } } /** * Determine if the given entity and observer is present in the table. * @param observer - the player observer. * @param entityID - ID of the entity. * @return TRUE if they are present, FALSE otherwise. */ protected boolean getMembership(Player observer, int entityID) { return observerEntityMap.contains(observer.getEntityId(), entityID); } /** * Determine if a given entity is visible for a particular observer. * @param observer - the observer player. * @param entityID - ID of the entity that we are testing for visibility. * @return TRUE if the entity is visible, FALSE otherwise. */ protected boolean isVisible(Player observer, int entityID) { // If we are using a whitelist, presence means visibility - if not, the opposite is the case boolean presence = getMembership(observer, entityID); return policy == Policy.WHITELIST ? presence : !presence; } /** * Remove the given entity from the underlying map. * @param entity - the entity to remove. * @param destroyed - TRUE if the entity was killed, FALSE if it is merely unloading. */ protected void removeEntity(Entity entity, boolean destroyed) { int entityID = entity.getEntityId(); for (Map<Integer, Boolean> maps : observerEntityMap.rowMap().values()) { maps.remove(entityID); } } /** * Invoked when a player logs out. * @param player - the player that jused logged out. */ protected void removePlayer(Player player) { // Cleanup observerEntityMap.rowMap().remove(player.getEntityId()); } /** * Construct the Bukkit event listener. * @return Our listener. */ private Listener constructBukkit() { return new Listener() { @EventHandler public void onEntityDeath(EntityDeathEvent e) { removeEntity(e.getEntity(), true); } @EventHandler public void onChunkUnload(ChunkUnloadEvent e) { for (Entity entity : e.getChunk().getEntities()) { removeEntity(entity, false); } } @EventHandler public void onPlayerQuit(PlayerQuitEvent e) { removePlayer(e.getPlayer()); } }; } /** * Construct the packet listener that will be used to intercept every entity-related packet. * @param plugin - the parent plugin. * @return The packet listener. */ private PacketAdapter constructProtocol(Plugin plugin) { return new PacketAdapter(plugin, ENTITY_PACKETS) { @Override public void onPacketSending(PacketEvent event) { int entityID = event.getPacket().getIntegers().read(0); // See if this packet should be cancelled if (!isVisible(event.getPlayer(), entityID)) { event.setCancelled(true); } } }; } /** * Toggle the visibility status of an entity for a player. * <p> * If the entity is visible, it will be hidden. If it is hidden, it will become visible. * @param observer - the player observer. * @param entity - the entity to toggle. * @return TRUE if the entity was visible before, FALSE otherwise. */ public final boolean toggleEntity(Player observer, Entity entity) { if (isVisible(observer, entity.getEntityId())) { return hideEntity(observer, entity); } else { return !showEntity(observer, entity); } } /** * Allow the observer to see an entity that was previously hidden. * @param observer - the observer. * @param entity - the entity to show. * @return TRUE if the entity was hidden before, FALSE otherwise. */ public final boolean showEntity(Player observer, Entity entity) { validate(observer, entity); boolean hiddenBefore = !setVisibility(observer, entity.getEntityId(), true); // Resend packets if (manager != null && hiddenBefore) { manager.updateEntity(entity, Arrays.asList(observer)); } return hiddenBefore; } /** * Prevent the observer from seeing a given entity. * @param observer - the player observer. * @param entity - the entity to hide. * @return TRUE if the entity was previously visible, FALSE otherwise. */ public final boolean hideEntity(Player observer, Entity entity) { validate(observer, entity); boolean visibleBefore = setVisibility(observer, entity.getEntityId(), false); if (visibleBefore) { PacketContainer destroyEntity = new PacketContainer(ENTITY_DESTROY); destroyEntity.getIntegerArrays().write(0, new int[] { entity.getEntityId() }); // Make the entity disappear try { manager.sendServerPacket(observer, destroyEntity); } catch (InvocationTargetException e) { throw new RuntimeException("Cannot send server packet.", e); } } return visibleBefore; } /** * Determine if the given entity has been hidden from an observer. * <p> * Note that the entity may very well be occluded or out of range from the perspective * of the observer. This method simply checks if an entity has been completely hidden * for that observer. * @param observer - the observer. * @param entity - the entity that may be hidden. * @return TRUE if the player may see the entity, FALSE if the entity has been hidden. */ public final boolean canSee(Player observer, Entity entity) { validate(observer, entity); return isVisible(observer, entity.getEntityId()); } // For valdiating the input parameters private void validate(Player observer, Entity entity) { Preconditions.checkNotNull(observer, "observer cannot be NULL."); Preconditions.checkNotNull(entity, "entity cannot be NULL."); } /** * Retrieve the current visibility policy. * @return The current visibility policy. */ public Policy getPolicy() { return policy; } public void close() { if (manager != null) { HandlerList.unregisterAll(bukkitListener); manager.removePacketListener(protocolListener); manager = null; } } }