diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java index d96d085c..d1c7cc98 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RDisplay.java @@ -26,6 +26,7 @@ import net.minecraft.network.syncher.EntityDataSerializers; import org.bukkit.Color; import org.bukkit.Location; import org.bukkit.entity.Display; +import org.bukkit.entity.Player; import org.bukkit.entity.EntityType; import org.bukkit.util.Transformation; import org.joml.Quaternionf; @@ -135,6 +136,15 @@ public abstract class RDisplay extends REntity { public void setViewRange(float viewRange) { this.viewRange = viewRange; sendPacket(updatePacketSink, this::getViewRange); + server.updateViewers(this); + } + + /** + * A view range of 1 equals 64 blocks. This is only a comparison, so huge values like {@link Float#MAX_VALUE} are cheap. + */ + @Override + protected boolean canView(Player player, int chunkDistance) { + return chunkDistance <= Math.ceil(viewRange * 64.0 / 16.0); } private static final EntityDataAccessor viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java index bbab5bbf..5d6f422f 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntity.java @@ -39,6 +39,7 @@ import org.bukkit.Location; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.util.CraftNamespacedKey; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.util.*; @@ -56,6 +57,10 @@ public class REntity { private static final Random random = new Random(); protected final REntityServer server; + /** + * Players this entity is currently spawned for, maintained by the {@link REntityServer}. + */ + final Set viewers = new HashSet<>(); private final EntityType entityType; @Getter protected final int entityId; @@ -120,6 +125,16 @@ public class REntity { this.objectData = objectData; } + /** + * Whether the given player should see this entity. + * Defaults to the view distance of the player. + * + * @param chunkDistance the larger of the chunk distances between the player and the entity on the x and z axis + */ + protected boolean canView(Player player, int chunkDistance) { + return chunkDistance <= server.viewRadius(player); + } + public void hide(boolean hide) { if (hidden == hide) return; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java index bff64bec..046c55f5 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -42,14 +42,8 @@ import java.util.stream.Collectors; public class REntityServer implements Listener { - private static final HashSet emptyEntities = new HashSet<>(0); - private static final HashSet emptyPlayers = new HashSet<>(0); - private final ConcurrentHashMap entityMap = new ConcurrentHashMap<>(); - private final HashMap> entities = new HashMap<>(); - private final HashMap> players = new HashMap<>(); private final HashMap lastLocation = new HashMap<>(); - private final HashMap viewDistance = new HashMap<>(); private final Set playersThatClicked = Collections.synchronizedSet(new HashSet<>()); @@ -78,20 +72,22 @@ public class REntityServer implements Listener { public void addPlayer(Player player) { Location location = player.getLocation(); lastLocation.put(player, location); - viewDistance.put(player, viewRadius(player)); - forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z)); + updateViewer(player, posToChunk(location.getX()), posToChunk(location.getZ())); } public void removePlayer(Player player) { - if (!viewDistance.containsKey(player)) { + if (lastLocation.remove(player) == null) { return; } - forChunkInView(player, lastLocation.remove(player), (x, z) -> removePlayerFromChunk(player, x, z)); - viewDistance.remove(player); + for (REntity entity : entityMap.values()) { + if (entity.viewers.remove(player)) { + hideFrom(entity, player); + } + } } public List getPlayers() { - return new ArrayList<>(viewDistance.keySet()); + return new ArrayList<>(lastLocation.keySet()); } public void close() { @@ -104,43 +100,53 @@ public class REntityServer implements Listener { void addEntity(REntity entity) { entityMap.put(entity.entityId, entity); - addEntityToChunk(entity); + updateViewers(entity); entity.list(packet -> updateEntity(entity, packet)); entity.spawn(packet -> updateEntity(entity, packet)); } void preEntityMove(REntity entity, double toX, double toZ) { - long fromId = entityToId(entity); - long toId = posToId(toX, toZ); - if (fromId == toId) return; + if (posToChunk(entity.x()) == posToChunk(toX) && posToChunk(entity.z()) == posToChunk(toZ)) return; - if (!entity.isHidden()) { - onMissing(players.get(fromId), players.get(toId), entity::despawn); - } - onMissing(players.get(fromId), players.get(toId), entity::delist); - removeEntityFromChunk(entity); + int chunkX = posToChunk(toX); + int chunkZ = posToChunk(toZ); + entity.viewers.removeIf(player -> { + if (inView(entity, player, chunkX, chunkZ)) return false; + hideFrom(entity, player); + return true; + }); } void postEntityMove(REntity entity, double fromX, double fromZ) { - long fromId = posToId(fromX, fromZ); - long toId = entityToId(entity); - if (fromId == toId) return; + if (posToChunk(fromX) == posToChunk(entity.x()) && posToChunk(fromZ) == posToChunk(entity.z())) return; - addEntityToChunk(entity); - onMissing(players.get(toId), players.get(fromId), entity::list); - if (!entity.isHidden()) { - onMissing(players.get(toId), players.get(fromId), entity::spawn); - } + addMissingViewers(entity); } void updateEntity(REntity entity, Object packet) { if (entity.isHidden()) return; - for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + for (Player player : entity.viewers) { TinyProtocol.instance.sendPacket(player, packet); } } + /** + * Re-evaluates which players see the entity, e.g. after its view range changed. + */ + void updateViewers(REntity entity) { + if (!entityMap.containsKey(entity.entityId)) return; + + int chunkX = posToChunk(entity.x()); + int chunkZ = posToChunk(entity.z()); + entity.viewers.removeIf(player -> { + if (inView(entity, player, chunkX, chunkZ)) return false; + hideFrom(entity, player); + return true; + }); + addMissingViewers(entity); + } + /** * Re-sends list and spawn packets of an already added entity to all current viewers, * e.g. when information that is part of the tab list entry changed after the entity was added. @@ -148,7 +154,7 @@ public class REntityServer implements Listener { void refreshEntity(REntity entity) { if (!entityMap.containsKey(entity.entityId)) return; - for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + for (Player player : entity.viewers) { Consumer sink = packet -> TinyProtocol.instance.sendPacket(player, packet); if (!entity.isHidden()) entity.despawn(sink); entity.delist(sink); @@ -159,11 +165,12 @@ public class REntityServer implements Listener { void removeEntity(REntity entity) { entity.despawn(packet -> updateEntity(entity, packet)); - removeEntityFromChunk(entity); entity.delist(packet -> updateEntity(entity, packet)); + entity.viewers.clear(); entityMap.remove(entity.entityId); } + public List getEntities() { return new ArrayList<>(entityMap.values()); } @@ -172,18 +179,6 @@ public class REntityServer implements Listener { return entityMap.values().stream().filter(clazz::isInstance).map(clazz::cast).collect(Collectors.toList()); } - private void addEntityToChunk(REntity entity) { - entities.computeIfAbsent(entityToId(entity), i -> new HashSet<>()).add(entity); - } - - private void removeEntityFromChunk(REntity entity) { - long id = entityToId(entity); - HashSet entitiesInChunk = entities.get(id); - if (entitiesInChunk == null) return; - entitiesInChunk.remove(entity); - if (entitiesInChunk.isEmpty()) entities.remove(id); - } - @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) public void onMove(PlayerMoveEvent e) { Player player = e.getPlayer(); @@ -191,90 +186,63 @@ public class REntityServer implements Listener { Location to = e.getTo(); if (from == null || to == null) return; - int fromX = posToChunk(from.getX()); - int fromZ = posToChunk(from.getZ()); int toX = posToChunk(to.getX()); int toZ = posToChunk(to.getZ()); - if (fromX == toX && fromZ == toZ) return; + if (posToChunk(from.getX()) == toX && posToChunk(from.getZ()) == toZ) return; lastLocation.put(player, to); - - int toViewDistance = viewRadius(player); - forChunkInView(player, from, (x, z) -> { - if (Math.abs(x - toX) > toViewDistance || Math.abs(z - toZ) > toViewDistance) { - removePlayerFromChunk(player, x, z); - } - }); - - int fromViewDistance = this.viewDistance.put(player, toViewDistance); - forChunkInView(player, to, (x, z) -> { - if (Math.abs(x - fromX) > fromViewDistance || Math.abs(z - fromZ) > fromViewDistance) { - addPlayerToChunk(player, x, z); - } - }); + updateViewer(player, toX, toZ); } @EventHandler public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); - Location location = lastLocation.remove(player); - if (location == null) return; + if (lastLocation.remove(player) == null) return; - forChunkInView(player, location, (x, z) -> { - long id = chunkToId(x, z); - Set playersInChunk = players.get(id); - playersInChunk.remove(player); - if (playersInChunk.isEmpty()) players.remove(id); - }); - viewDistance.remove(player); + for (REntity entity : entityMap.values()) { + entity.viewers.remove(player); + } } - private void onMissing(Set of, Set in, Consumer> packetProvider) { - if (of == null) return; - - for (Player player : of) { - if (in == null || !in.contains(player)) { - packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet)); + private void updateViewer(Player player, int chunkX, int chunkZ) { + for (REntity entity : entityMap.values()) { + boolean inView = inView(entity, player, chunkX, chunkZ); + if (inView) { + if (entity.viewers.add(player)) showTo(entity, player); + } else if (entity.viewers.remove(player)) { + hideFrom(entity, player); } } } - private void forChunkInView(Player player, Location location, BiConsumer func) { - int chunkX = posToChunk(location.getX()); - int chunkZ = posToChunk(location.getZ()); - int viewDistance = this.viewDistance.get(player); - - for (int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) { - for (int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) { - func.accept(x, z); + private void addMissingViewers(REntity entity) { + int chunkX = posToChunk(entity.x()); + int chunkZ = posToChunk(entity.z()); + for (Player player : lastLocation.keySet()) { + if (inView(entity, player, chunkX, chunkZ) && entity.viewers.add(player)) { + showTo(entity, player); } } } - private void addPlayerToChunk(Player player, int x, int z) { - long id = chunkToId(x, z); - players.computeIfAbsent(id, i -> new HashSet<>()).add(player); - for (REntity entity : entities.getOrDefault(id, emptyEntities)) { - entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet)); - if (!entity.isHidden()) { - entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); - } + private boolean inView(REntity entity, Player player, int entityChunkX, int entityChunkZ) { + Location location = lastLocation.get(player); + int distance = Math.max(Math.abs(posToChunk(location.getX()) - entityChunkX), Math.abs(posToChunk(location.getZ()) - entityChunkZ)); + return entity.canView(player, distance); + } + + private void showTo(REntity entity, Player player) { + entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet)); + if (!entity.isHidden()) { + entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); } } - private void removePlayerFromChunk(Player player, int x, int z) { - long id = chunkToId(x, z); - - Set playersInChunk = players.get(id); - playersInChunk.remove(player); - if (playersInChunk.isEmpty()) players.remove(id); - - for (REntity entity : entities.getOrDefault(id, emptyEntities)) { - if (!entity.isHidden()) { - entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); - } - entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet)); + private void hideFrom(REntity entity, Player player) { + if (!entity.isHidden()) { + entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet)); } + entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet)); } public void tick() { @@ -287,19 +255,10 @@ public class REntityServer implements Listener { return (int) (coord / 16) - (coord < 0 ? 1 : 0); } - private int viewRadius(Player player) { + /** + * Default view radius in chunks for entities without a view range of their own. + */ + int viewRadius(Player player) { return player.getClientViewDistance() / 2; } - - private long entityToId(REntity entity) { - return posToId(entity.x(), entity.z()); - } - - private long posToId(double x, double z) { - return chunkToId(posToChunk(x), posToChunk(z)); - } - - private long chunkToId(int x, int z) { - return ((long) x << 32) + z; - } }