1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
|
package net.tylermurphy.hideAndSeek.game;
import static com.comphenix.protocol.PacketType.Play.Server.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
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();
private static final PacketType[] ENTITY_PACKETS = {
ENTITY_EQUIPMENT, 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_TELEPORT, ENTITY_HEAD_ROTATION, ENTITY_STATUS, ATTACH_ENTITY, ENTITY_METADATA,
ENTITY_EFFECT, REMOVE_ENTITY_EFFECT, BLOCK_BREAK_ANIMATION
};
public enum Policy {
WHITELIST,
BLACKLIST,
}
private ProtocolManager manager;
private final Listener bukkitListener;
private final PacketAdapter protocolListener;
protected final Policy 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 entityID - 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.
*/
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;
}
/**
* Remove the given entity from the underlying map.
* @param entity - the entity to remove.
*/
protected void removeEntity(Entity entity) {
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());
}
@EventHandler
public void onChunkUnload(ChunkUnloadEvent e) {
for (Entity entity : e.getChunk().getEntities()) {
removeEntity(entity);
}
}
@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.
*/
@SuppressWarnings("unused")
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, Collections.singletonList(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);
try {
destroyEntity.getIntegerArrays().write(0, new int[]{entity.getEntityId()});
} catch (Exception e){ return false; }
// Make the entity disappear
manager.sendServerPacket(observer, destroyEntity);
}
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.
*/
@SuppressWarnings("unused")
public final boolean canSee(Player observer, Entity entity) {
validate(observer, entity);
return isVisible(observer, entity.getEntityId());
}
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.
*/
@SuppressWarnings("unused")
public Policy getPolicy() {
return policy;
}
@SuppressWarnings("unused")
public void close() {
if (manager != null) {
HandlerList.unregisterAll(bukkitListener);
manager.removePacketListener(protocolListener);
manager = null;
}
}
}
|