Skip to content

Instantly share code, notes, and snippets.

@dmulloy2
Forked from aadnk/EntityHider.java
Last active August 18, 2022 08:25
Show Gist options
  • Save dmulloy2/5526f5bf906c064c255e to your computer and use it in GitHub Desktop.
Save dmulloy2/5526f5bf906c064c255e to your computer and use it in GitHub Desktop.
package net.dmulloy2.example;
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, UPDATE_ENTITY_NBT, COMBAT_EVENT
// 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 - the plugin that controls this entity hider.
* @param policy - the default visibility 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));
}
/**
* Set the visibility status of a given entity for a particular observer.
* @param observer - the observer player.
* @param entity - ID of the entity that will be hidden or made visible.
* @param visible - TRUE if the entity should be made visible, FALSE if not.
* @return TRUE if the entity was visible before this method call, FALSE otherwise.
*/
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 index = event.getPacketType() == COMBAT_EVENT ? 1 : 0;
Integer entityID = event.getPacket().getIntegers().readSafely(index);
if (entityID != null) {
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;
}
}
}
@J0B10
Copy link

J0B10 commented Oct 18, 2018

We use this EntityHider in Co0sh/BetonQuest and got a rare bug reported that is propably caused by a bug in this entity hider:

[01:10:40] [Server thread/INFO]: ParadiddlePride issued server command: /tp Its_Austin
[01:10:40] [Server thread/INFO]: [ParadiddlePride: Teleported ParadiddlePride to Its_Austin]
[01:10:40] [Server thread/ERROR]: Could not pass event ChunkUnloadEvent to BetonQuest v2.0-preview.1
org.bukkit.event.EventException: null
	at org.bukkit.plugin.java.JavaPluginLoader$1.execute(JavaPluginLoader.java:308) ~[spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at org.bukkit.plugin.RegisteredListener.callEvent(RegisteredListener.java:62) ~[spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at org.bukkit.plugin.SimplePluginManager.fireEvent(SimplePluginManager.java:500) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:485) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.ChunkProviderServer.unloadChunk(ChunkProviderServer.java:313) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.ChunkProviderServer.unloadChunks(ChunkProviderServer.java:288) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.WorldServer.doTick(WorldServer.java:286) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.MinecraftServer.b(MinecraftServer.java:956) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.DedicatedServer.b(DedicatedServer.java:417) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.MinecraftServer.a(MinecraftServer.java:835) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at net.minecraft.server.v1_13_R2.MinecraftServer.run(MinecraftServer.java:733) [spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at java.lang.Thread.run(Thread.java:745) [?:1.8.0_91]
Caused by: java.util.ConcurrentModificationException
	at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:711) ~[?:1.8.0_91]
	at java.util.LinkedHashMap$LinkedKeyIterator.next(LinkedHashMap.java:734) ~[?:1.8.0_91]
	at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	at pl.betoncraft.betonquest.compatibility.protocollib.EntityHider.removeEntity(EntityHider.java:201) ~[?:?]
	at pl.betoncraft.betonquest.compatibility.protocollib.EntityHider$1.onChunkUnload(EntityHider.java:233) ~[?:?]
	at sun.reflect.GeneratedMethodAccessor66.invoke(Unknown Source) ~[?:?]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_91]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_91]
	at org.bukkit.plugin.java.JavaPluginLoader$1.execute(JavaPluginLoader.java:304) ~[spigot-1.13.1.jar:git-Spigot-f6a273b-1cf8b5d]
	... 11 more

We are now looking in fixing it, any help would be apreciated as we are not that used to ProtocolLibs API.
I'll leave another comment when we made progress on fixing it.

The original issue: BetonQuest/BetonQuest#873

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment