Entity Shown only to specific Player/s

Discussion in 'Plugin Development' started by iZanax, Jun 26, 2013.

Thread Status:
Not open for further replies.
  1. Offline

    iZanax

    I'm searching for a similar function like player.showPlayer(Player) / player.hidePlayer(Player).
    In my case I want to show for example a Zombie only to Player A. and not for Player B.
    Is this possible? Probably due packets or ProticolLib ( Comphenix ).
    This should be really nice feature for many uses like showPlayer offers for Players.

    What can I use? What do you suggest?

    Thanks in advance,
    Zanax
     
  2. Offline

    Comphenix

    You can indeed do this in ProtocolLib (download):
    Code:java
    1. package com.comphenix.example;
    2.  
    3. import static com.comphenix.protocol.Packets.Server.*;
    4.  
    5. import java.lang.reflect.InvocationTargetException;
    6. import java.util.Arrays;
    7. import java.util.Map;
    8.  
    9. import org.bukkit.entity.Entity;
    10. import org.bukkit.entity.Player;
    11. import org.bukkit.event.EventHandler;
    12. import org.bukkit.event.HandlerList;
    13. import org.bukkit.event.Listener;
    14. import org.bukkit.event.entity.EntityDeathEvent;
    15. import org.bukkit.event.player.PlayerQuitEvent;
    16. import org.bukkit.event.world.ChunkUnloadEvent;
    17. import org.bukkit.plugin.Plugin;
    18.  
    19. import com.comphenix.protocol.ProtocolLibrary;
    20. import com.comphenix.protocol.ProtocolManager;
    21. import com.comphenix.protocol.events.ConnectionSide;
    22. import com.comphenix.protocol.events.PacketAdapter;
    23. import com.comphenix.protocol.events.PacketContainer;
    24. import com.comphenix.protocol.events.PacketEvent;
    25. import com.google.common.base.Preconditions;
    26. import com.google.common.collect.HashBasedTable;
    27. import com.google.common.collect.Table;
    28.  
    29. public class EntityHider implements Listener {
    30. protected Table<Integer, Integer, Boolean> observerEntityMap = HashBasedTable.create();
    31.  
    32. // Packets that update remote player entities
    33. private static final Integer[] ENTITY_PACKETS = {
    34. ENTITY_EQUIPMENT, ENTITY_LOCATION_ACTION, ARM_ANIMATION, NAMED_ENTITY_SPAWN,
    35. COLLECT, VEHICLE_SPAWN, MOB_SPAWN, ENTITY_PAINTING, ADD_EXP_ORB, ENTITY_VELOCITY,
    36. REL_ENTITY_MOVE, ENTITY_LOOK, REL_ENTITY_MOVE_LOOK, REL_ENTITY_MOVE_LOOK,
    37. ENTITY_TELEPORT, ENTITY_HEAD_ROTATION, ENTITY_STATUS, ATTACH_ENTITY, ENTITY_METADATA,
    38. MOB_EFFECT, REMOVE_MOB_EFFECT, BLOCK_BREAK_ANIMATION
    39.  
    40. // We don't handle DESTROY_ENTITY though
    41. };
    42.  
    43. /**
    44.   * The current entity visibility policy.
    45.   * @author Kristian
    46.   */
    47. public enum Policy {
    48. /**
    49.   * All entities are invisible by default. Only entities specifically made visible may be seen.
    50.   */
    51. WHITELIST,
    52.  
    53. /**
    54.   * All entities are visible by default. An entity can only be hidden explicitly.
    55.   */
    56. BLACKLIST,
    57. }
    58.  
    59. private ProtocolManager manager;
    60.  
    61. // Listeners
    62. private Listener bukkitListener;
    63. private PacketAdapter protocolListener;
    64.  
    65. // Current policy
    66. protected final Policy policy;
    67.  
    68. /**
    69.   * Construct a new entity hider.
    70.   * @param plugin - the plugin that controls this entity hider.
    71.   * @param policy - the default visibility policy.
    72.   */
    73. public EntityHider(Plugin plugin, Policy policy) {
    74. Preconditions.checkNotNull(plugin, "plugin cannot be NULL.");
    75.  
    76. // Save policy
    77. this.policy = policy;
    78. this.manager = ProtocolLibrary.getProtocolManager();
    79.  
    80. // Register events and packet listener
    81. plugin.getServer().getPluginManager().registerEvents(
    82. bukkitListener = constructBukkit(), plugin);
    83. manager.addPacketListener(
    84. protocolListener = constructProtocol(plugin));
    85. }
    86.  
    87. /**
    88.   * Set the visibility status of a given entity for a particular observer.
    89.   * @param observer - the observer player.
    90.   * @param entity - ID of the entity that will be hidden or made visible.
    91.   * @param visible - TRUE if the entity should be made visible, FALSE if not.
    92.   * @return TRUE if the entity was visible before this method call, FALSE otherwise.
    93.   */
    94. protected boolean setVisibility(Player observer, int entityID, boolean visible) {
    95. switch (policy) {
    96. case BLACKLIST:
    97. // Non-membership means they are visible
    98. return !setMembership(observer, entityID, !visible);
    99. case WHITELIST:
    100. return setMembership(observer, entityID, visible);
    101. default :
    102. throw new IllegalArgumentException("Unknown policy: " + policy);
    103. }
    104. }
    105.  
    106. /**
    107.   * Add or remove the given entity and observer entry from the table.
    108.   * @param observer - the player observer.
    109.   * @param entityID - ID of the entity.
    110.   * @param member - TRUE if they should be present in the table, FALSE otherwise.
    111.   * @return TRUE if they already were present, FALSE otherwise.
    112.   */
    113. // Helper method
    114. protected boolean setMembership(Player observer, int entityID, boolean member) {
    115. if (member) {
    116. return observerEntityMap.put(observer.getEntityId(), entityID, true) != null;
    117. } else {
    118. return observerEntityMap.remove(observer.getEntityId(), entityID) != null;
    119. }
    120. }
    121.  
    122. /**
    123.   * Determine if the given entity and observer is present in the table.
    124.   * @param observer - the player observer.
    125.   * @param entityID - ID of the entity.
    126.   * @return TRUE if they are present, FALSE otherwise.
    127.   */
    128. protected boolean getMembership(Player observer, int entityID) {
    129. return observerEntityMap.contains(observer.getEntityId(), entityID);
    130. }
    131.  
    132. /**
    133.   * Determine if a given entity is visible for a particular observer.
    134.   * @param observer - the observer player.
    135.   * @param entityID - ID of the entity that we are testing for visibility.
    136.   * @return TRUE if the entity is visible, FALSE otherwise.
    137.   */
    138. protected boolean isVisible(Player observer, int entityID) {
    139. // If we are using a whitelist, presence means visibility - if not, the opposite is the case
    140. boolean presence = getMembership(observer, entityID);
    141.  
    142. return policy == Policy.WHITELIST ? presence : !presence;
    143. }
    144.  
    145. /**
    146.   * Remove the given entity from the underlying map.
    147.   * @param entity - the entity to remove.
    148.   * @param destroyed - TRUE if the entity was killed, FALSE if it is merely unloading.
    149.   */
    150. protected void removeEntity(Entity entity, boolean destroyed) {
    151. int entityID = entity.getEntityId();
    152.  
    153. for (Map<Integer, Boolean> maps : observerEntityMap.rowMap().values()) {
    154. maps.remove(entityID);
    155. }
    156. }
    157.  
    158. /**
    159.   * Invoked when a player logs out.
    160.   * @param player - the player that jused logged out.
    161.   */
    162. protected void removePlayer(Player player) {
    163. // Cleanup
    164. observerEntityMap.rowMap().remove(player.getEntityId());
    165. }
    166.  
    167. /**
    168.   * Construct the Bukkit event listener.
    169.   * @return Our listener.
    170.   */
    171. private Listener constructBukkit() {
    172. return new Listener() {
    173. @EventHandler
    174. public void onEntityDeath(EntityDeathEvent e) {
    175. removeEntity(e.getEntity(), true);
    176. }
    177.  
    178. @EventHandler
    179. public void onChunkUnload(ChunkUnloadEvent e) {
    180. for (Entity entity : e.getChunk().getEntities()) {
    181. removeEntity(entity, false);
    182. }
    183. }
    184.  
    185. @EventHandler
    186. public void onPlayerQuit(PlayerQuitEvent e) {
    187. removePlayer(e.getPlayer());
    188. }
    189. };
    190. }
    191.  
    192. /**
    193.   * Construct the packet listener that will be used to intercept every entity-related packet.
    194.   * @param plugin - the parent plugin.
    195.   * @return The packet listener.
    196.   */
    197. private PacketAdapter constructProtocol(Plugin plugin) {
    198. return new PacketAdapter(plugin, ConnectionSide.SERVER_SIDE, ENTITY_PACKETS) {
    199. @Override
    200. public void onPacketSending(PacketEvent event) {
    201. int entityID = event.getPacket().getIntegers().read(0);
    202.  
    203. // See if this packet should be cancelled
    204. if (!isVisible(event.getPlayer(), entityID)) {
    205. event.setCancelled(true);
    206. }
    207. }
    208. };
    209. }
    210.  
    211. /**
    212.   * Toggle the visibility status of an entity for a player.
    213.   * <p>
    214.   * If the entity is visible, it will be hidden. If it is hidden, it will become visible.
    215.   * @param observer - the player observer.
    216.   * @param entity - the entity to toggle.
    217.   * @return TRUE if the entity was visible before, FALSE otherwise.
    218.   */
    219. public final boolean toggleEntity(Player observer, Entity entity) {
    220. if (isVisible(observer, entity.getEntityId())) {
    221. return hideEntity(observer, entity);
    222. } else {
    223. return !showEntity(observer, entity);
    224. }
    225. }
    226.  
    227. /**
    228.   * Allow the observer to see an entity that was previously hidden.
    229.   * @param observer - the observer.
    230.   * @param entity - the entity to show.
    231.   * @return TRUE if the entity was hidden before, FALSE otherwise.
    232.   */
    233. public final boolean showEntity(Player observer, Entity entity) {
    234. validate(observer, entity);
    235. boolean hiddenBefore = !setVisibility(observer, entity.getEntityId(), true);
    236.  
    237. // Resend packets
    238. if (manager != null && hiddenBefore) {
    239. manager.updateEntity(entity, Arrays.asList(observer));
    240. }
    241. return hiddenBefore;
    242. }
    243.  
    244. /**
    245.   * Prevent the observer from seeing a given entity.
    246.   * @param observer - the player observer.
    247.   * @param entity - the entity to hide.
    248.   * @return TRUE if the entity was previously visible, FALSE otherwise.
    249.   */
    250. public final boolean hideEntity(Player observer, Entity entity) {
    251. validate(observer, entity);
    252. boolean visibleBefore = setVisibility(observer, entity.getEntityId(), false);
    253.  
    254. if (visibleBefore) {
    255. PacketContainer destroyEntity = new PacketContainer(DESTROY_ENTITY);
    256. destroyEntity.getIntegerArrays().write(0, new int[] { entity.getEntityId() });
    257.  
    258. // Make the entity disappear
    259. try {
    260. manager.sendServerPacket(observer, destroyEntity);
    261. throw new RuntimeException("Cannot send server packet.", e);
    262. }
    263. }
    264. return visibleBefore;
    265. }
    266.  
    267. /**
    268.   * Determine if the given entity has been hidden from an observer.
    269.   * <p>
    270.   * Note that the entity may very well be occluded or out of range from the perspective
    271.   * of the observer. This method simply checks if an entity has been completely hidden
    272.   * for that observer.
    273.   * @param observer - the observer.
    274.   * @param entity - the entity that may be hidden.
    275.   * @return TRUE if the player may see the entity, FALSE if the entity has been hidden.
    276.   */
    277. public final boolean canSee(Player observer, Entity entity) {
    278. validate(observer, entity);
    279.  
    280. return isVisible(observer, entity.getEntityId());
    281. }
    282.  
    283. // For valdiating the input parameters
    284. private void validate(Player observer, Entity entity) {
    285. Preconditions.checkNotNull(observer, "observer cannot be NULL.");
    286. Preconditions.checkNotNull(entity, "entity cannot be NULL.");
    287. }
    288.  
    289. /**
    290.   * Retrieve the current visibility policy.
    291.   * @return The current visibility policy.
    292.   */
    293. public Policy getPolicy() {
    294. return policy;
    295. }
    296.  
    297. public void close() {
    298. if (manager != null) {
    299. HandlerList.unregisterAll(bukkitListener);
    300. manager.removePacketListener(protocolListener);
    301. manager = null;
    302. }
    303. }
    304. }
    305.  


    Here's an example of how to use it:
    Code:java
    1. public class ExampleMod extends JavaPlugin {
    2. private EntityHider entityHider;
    3.  
    4. private static final int TICKS_PER_SECOND = 20;
    5.  
    6. @Override
    7. public void onEnable() {
    8. entityHider = new EntityHider(this, Policy.BLACKLIST);
    9. }
    10.  
    11. @Override
    12. public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
    13. if (sender instanceof Player) {
    14. final Player player = (Player) sender;
    15. final Sheep sheep = player.getWorld().spawn(player.getLocation(), Sheep.class);
    16.  
    17. // Show a particular entity
    18. entityHider.toggleEntity(player, sheep);
    19.  
    20. getServer().getScheduler().scheduleSyncDelayedTask(this, new Runnable() {
    21. @Override
    22. public void run() {
    23. entityHider.toggleEntity(player, sheep);
    24. }
    25. }, 10 * TICKS_PER_SECOND);
    26. }
    27. return true;
    28. }
    29. }

    Note that I haven't handled mob sounds in this version, so invisible mobs will still be audible. You can correct this by intercepting Packet62NamedSoundEffect, and cancel the packet if it corresponds to the location of a hidden entity.
     
    iZanax and blablubbabc like this.
  3. Offline

    iZanax

    Thanks a lot!
    This can contribute to many new creative ideas!
     
    Comphenix likes this.
  4. Offline

    iZanax

    I got one question, for my usage of this technique.

    I would like to use this the other way around.
    What I mean is to hide all spawning mobs for all players.
    And use the methods to make specific mobs show for a player.

    Is here an easy and efficient way to do this?
    Thanks in advance.

    Comphenix
     
  5. Offline

    Comphenix

    In general, I'd recommend doing some rudimentary profiling before optimizing anything. It may not be necessary.

    In this case, the biggest overhead is probably memory. Testing for membership in a Hashtable is O(1), so having more entries in the multi-table (more hidden entities per player) shouldn't increase processing time. But it may consume more memory - for instance, if you assume a server with 50 online players and 1500 loaded entities, the table will contain 50 * 1500 = 75000 entries.

    Internally, the HashBasedTable stores each "row" as a seperate HashTable, so the overhead associated with each row is negligible. Each entry within theses tables have about 32 bytes of overhead, along with the Integer value for each entry (16 or 24 bytes). The Boolean is just a simple singleton reference, so I don't believe it adds any additional memory.Together, this is 75000 * (32 + 24) = 4.2 MB. Keep in mind that this is probably the upper bound of what you will consume in memory.

    If you want, you could turn the "hiddenEntities" table into "visibileEntities" table, and only allow entities to be seen if they're in the table. That should be fairly to change in the original code.
     
  6. Offline

    iZanax

    Ah okay. That makes a lot of sense.
    Thanks for the excellent explanation.

    One question about ProticolLib.

    - When a player get in a chunk where an entity is who is hidden from him. Is this handled by ProticolLib? (So the entity is invisible for him)
    - Could I use ' private Listener constructBukkit() {' to addPlayerLoginEvent in it. To hide all current entities for him, like I explained for my usage?
    EDIT: How could I apply all hidden entities to the Joining Players of the Table?

    Thanks in Advance, Comphenix
     
  7. Offline

    Comphenix

    It could be, but the current version will automatically clear the hidden status of any unloaded (and killed) entities, so your plugin has to detect it and then hide them.

    But you can just as well do that with your own listener.

    However, I've extended my original EntityHider to now have a Policy enum. There you can select whether or not entities should be hidden by default, or visible by default (as they are now). In your case, set the enum to WHITELIST.

    The updated version is on GitHub:
    https://gist.github.com/aadnk/5871793
     
Thread Status:
Not open for further replies.

Share This Page