From 3fe2e006b910c811a1f19a8566eb6bfae73cffd2 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Mon, 19 Oct 2026 08:32:16 +0000 Subject: [PATCH] REntityServer: use per-entity view range instead of chunk view distance Entities now decide per player whether they are visible. REntity defaults to the player view radius, RDisplay derives it from its viewRange (1 = 64 blocks) capped at the client view distance, so values like Float.MAX_VALUE stay cheap. Changing the viewRange re-evaluates the viewers. Fixes #488 --- .../src/de/steamwar/entity/RDisplay.java | 10 + .../src/de/steamwar/entity/REntity.java | 15 ++ .../src/de/steamwar/entity/REntityServer.java | 193 +++++++----------- 3 files changed, 101 insertions(+), 117 deletions(-) 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; - } }