forked from SteamWar/SteamWar
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
This commit is contained in:
@@ -26,6 +26,7 @@ import net.minecraft.network.syncher.EntityDataSerializers;
|
|||||||
import org.bukkit.Color;
|
import org.bukkit.Color;
|
||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.entity.Display;
|
import org.bukkit.entity.Display;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.util.Transformation;
|
import org.bukkit.util.Transformation;
|
||||||
import org.joml.Quaternionf;
|
import org.joml.Quaternionf;
|
||||||
@@ -135,6 +136,15 @@ public abstract class RDisplay extends REntity {
|
|||||||
public void setViewRange(float viewRange) {
|
public void setViewRange(float viewRange) {
|
||||||
this.viewRange = viewRange;
|
this.viewRange = viewRange;
|
||||||
sendPacket(updatePacketSink, this::getViewRange);
|
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<Float> viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT);
|
private static final EntityDataAccessor<Float> viewRangeWatcher = new EntityDataAccessor<>(17, EntityDataSerializers.FLOAT);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import org.bukkit.Location;
|
|||||||
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
||||||
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -56,6 +57,10 @@ public class REntity {
|
|||||||
private static final Random random = new Random();
|
private static final Random random = new Random();
|
||||||
|
|
||||||
protected final REntityServer server;
|
protected final REntityServer server;
|
||||||
|
/**
|
||||||
|
* Players this entity is currently spawned for, maintained by the {@link REntityServer}.
|
||||||
|
*/
|
||||||
|
final Set<Player> viewers = new HashSet<>();
|
||||||
private final EntityType entityType;
|
private final EntityType entityType;
|
||||||
@Getter
|
@Getter
|
||||||
protected final int entityId;
|
protected final int entityId;
|
||||||
@@ -120,6 +125,16 @@ public class REntity {
|
|||||||
this.objectData = objectData;
|
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) {
|
public void hide(boolean hide) {
|
||||||
if (hidden == hide) return;
|
if (hidden == hide) return;
|
||||||
|
|
||||||
|
|||||||
@@ -42,14 +42,8 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class REntityServer implements Listener {
|
public class REntityServer implements Listener {
|
||||||
|
|
||||||
private static final HashSet<REntity> emptyEntities = new HashSet<>(0);
|
|
||||||
private static final HashSet<Player> emptyPlayers = new HashSet<>(0);
|
|
||||||
|
|
||||||
private final ConcurrentHashMap<Integer, REntity> entityMap = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<Integer, REntity> entityMap = new ConcurrentHashMap<>();
|
||||||
private final HashMap<Long, HashSet<REntity>> entities = new HashMap<>();
|
|
||||||
private final HashMap<Long, Set<Player>> players = new HashMap<>();
|
|
||||||
private final HashMap<Player, Location> lastLocation = new HashMap<>();
|
private final HashMap<Player, Location> lastLocation = new HashMap<>();
|
||||||
private final HashMap<Player, Integer> viewDistance = new HashMap<>();
|
|
||||||
|
|
||||||
private final Set<Player> playersThatClicked = Collections.synchronizedSet(new HashSet<>());
|
private final Set<Player> playersThatClicked = Collections.synchronizedSet(new HashSet<>());
|
||||||
|
|
||||||
@@ -78,20 +72,22 @@ public class REntityServer implements Listener {
|
|||||||
public void addPlayer(Player player) {
|
public void addPlayer(Player player) {
|
||||||
Location location = player.getLocation();
|
Location location = player.getLocation();
|
||||||
lastLocation.put(player, location);
|
lastLocation.put(player, location);
|
||||||
viewDistance.put(player, viewRadius(player));
|
updateViewer(player, posToChunk(location.getX()), posToChunk(location.getZ()));
|
||||||
forChunkInView(player, location, (x, z) -> addPlayerToChunk(player, x, z));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removePlayer(Player player) {
|
public void removePlayer(Player player) {
|
||||||
if (!viewDistance.containsKey(player)) {
|
if (lastLocation.remove(player) == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
forChunkInView(player, lastLocation.remove(player), (x, z) -> removePlayerFromChunk(player, x, z));
|
for (REntity entity : entityMap.values()) {
|
||||||
viewDistance.remove(player);
|
if (entity.viewers.remove(player)) {
|
||||||
|
hideFrom(entity, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Player> getPlayers() {
|
public List<Player> getPlayers() {
|
||||||
return new ArrayList<>(viewDistance.keySet());
|
return new ArrayList<>(lastLocation.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
@@ -104,43 +100,53 @@ public class REntityServer implements Listener {
|
|||||||
|
|
||||||
void addEntity(REntity entity) {
|
void addEntity(REntity entity) {
|
||||||
entityMap.put(entity.entityId, entity);
|
entityMap.put(entity.entityId, entity);
|
||||||
addEntityToChunk(entity);
|
updateViewers(entity);
|
||||||
entity.list(packet -> updateEntity(entity, packet));
|
entity.list(packet -> updateEntity(entity, packet));
|
||||||
entity.spawn(packet -> updateEntity(entity, packet));
|
entity.spawn(packet -> updateEntity(entity, packet));
|
||||||
}
|
}
|
||||||
|
|
||||||
void preEntityMove(REntity entity, double toX, double toZ) {
|
void preEntityMove(REntity entity, double toX, double toZ) {
|
||||||
long fromId = entityToId(entity);
|
if (posToChunk(entity.x()) == posToChunk(toX) && posToChunk(entity.z()) == posToChunk(toZ)) return;
|
||||||
long toId = posToId(toX, toZ);
|
|
||||||
if (fromId == toId) return;
|
|
||||||
|
|
||||||
if (!entity.isHidden()) {
|
int chunkX = posToChunk(toX);
|
||||||
onMissing(players.get(fromId), players.get(toId), entity::despawn);
|
int chunkZ = posToChunk(toZ);
|
||||||
}
|
entity.viewers.removeIf(player -> {
|
||||||
onMissing(players.get(fromId), players.get(toId), entity::delist);
|
if (inView(entity, player, chunkX, chunkZ)) return false;
|
||||||
removeEntityFromChunk(entity);
|
hideFrom(entity, player);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void postEntityMove(REntity entity, double fromX, double fromZ) {
|
void postEntityMove(REntity entity, double fromX, double fromZ) {
|
||||||
long fromId = posToId(fromX, fromZ);
|
if (posToChunk(fromX) == posToChunk(entity.x()) && posToChunk(fromZ) == posToChunk(entity.z())) return;
|
||||||
long toId = entityToId(entity);
|
|
||||||
if (fromId == toId) return;
|
|
||||||
|
|
||||||
addEntityToChunk(entity);
|
addMissingViewers(entity);
|
||||||
onMissing(players.get(toId), players.get(fromId), entity::list);
|
|
||||||
if (!entity.isHidden()) {
|
|
||||||
onMissing(players.get(toId), players.get(fromId), entity::spawn);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateEntity(REntity entity, Object packet) {
|
void updateEntity(REntity entity, Object packet) {
|
||||||
if (entity.isHidden()) return;
|
if (entity.isHidden()) return;
|
||||||
|
|
||||||
for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) {
|
for (Player player : entity.viewers) {
|
||||||
TinyProtocol.instance.sendPacket(player, packet);
|
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,
|
* 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.
|
* 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) {
|
void refreshEntity(REntity entity) {
|
||||||
if (!entityMap.containsKey(entity.entityId)) return;
|
if (!entityMap.containsKey(entity.entityId)) return;
|
||||||
|
|
||||||
for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) {
|
for (Player player : entity.viewers) {
|
||||||
Consumer<Object> sink = packet -> TinyProtocol.instance.sendPacket(player, packet);
|
Consumer<Object> sink = packet -> TinyProtocol.instance.sendPacket(player, packet);
|
||||||
if (!entity.isHidden()) entity.despawn(sink);
|
if (!entity.isHidden()) entity.despawn(sink);
|
||||||
entity.delist(sink);
|
entity.delist(sink);
|
||||||
@@ -159,11 +165,12 @@ public class REntityServer implements Listener {
|
|||||||
|
|
||||||
void removeEntity(REntity entity) {
|
void removeEntity(REntity entity) {
|
||||||
entity.despawn(packet -> updateEntity(entity, packet));
|
entity.despawn(packet -> updateEntity(entity, packet));
|
||||||
removeEntityFromChunk(entity);
|
|
||||||
entity.delist(packet -> updateEntity(entity, packet));
|
entity.delist(packet -> updateEntity(entity, packet));
|
||||||
|
entity.viewers.clear();
|
||||||
entityMap.remove(entity.entityId);
|
entityMap.remove(entity.entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public List<REntity> getEntities() {
|
public List<REntity> getEntities() {
|
||||||
return new ArrayList<>(entityMap.values());
|
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());
|
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<REntity> entitiesInChunk = entities.get(id);
|
|
||||||
if (entitiesInChunk == null) return;
|
|
||||||
entitiesInChunk.remove(entity);
|
|
||||||
if (entitiesInChunk.isEmpty()) entities.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||||
public void onMove(PlayerMoveEvent e) {
|
public void onMove(PlayerMoveEvent e) {
|
||||||
Player player = e.getPlayer();
|
Player player = e.getPlayer();
|
||||||
@@ -191,90 +186,63 @@ public class REntityServer implements Listener {
|
|||||||
Location to = e.getTo();
|
Location to = e.getTo();
|
||||||
if (from == null || to == null) return;
|
if (from == null || to == null) return;
|
||||||
|
|
||||||
int fromX = posToChunk(from.getX());
|
|
||||||
int fromZ = posToChunk(from.getZ());
|
|
||||||
int toX = posToChunk(to.getX());
|
int toX = posToChunk(to.getX());
|
||||||
int toZ = posToChunk(to.getZ());
|
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);
|
lastLocation.put(player, to);
|
||||||
|
updateViewer(player, toX, toZ);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onQuit(PlayerQuitEvent e) {
|
public void onQuit(PlayerQuitEvent e) {
|
||||||
Player player = e.getPlayer();
|
Player player = e.getPlayer();
|
||||||
Location location = lastLocation.remove(player);
|
if (lastLocation.remove(player) == null) return;
|
||||||
if (location == null) return;
|
|
||||||
|
|
||||||
forChunkInView(player, location, (x, z) -> {
|
for (REntity entity : entityMap.values()) {
|
||||||
long id = chunkToId(x, z);
|
entity.viewers.remove(player);
|
||||||
Set<Player> playersInChunk = players.get(id);
|
}
|
||||||
playersInChunk.remove(player);
|
|
||||||
if (playersInChunk.isEmpty()) players.remove(id);
|
|
||||||
});
|
|
||||||
viewDistance.remove(player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMissing(Set<Player> of, Set<Player> in, Consumer<Consumer<Object>> packetProvider) {
|
private void updateViewer(Player player, int chunkX, int chunkZ) {
|
||||||
if (of == null) return;
|
for (REntity entity : entityMap.values()) {
|
||||||
|
boolean inView = inView(entity, player, chunkX, chunkZ);
|
||||||
for (Player player : of) {
|
if (inView) {
|
||||||
if (in == null || !in.contains(player)) {
|
if (entity.viewers.add(player)) showTo(entity, player);
|
||||||
packetProvider.accept(packet -> TinyProtocol.instance.sendPacket(player, packet));
|
} else if (entity.viewers.remove(player)) {
|
||||||
|
hideFrom(entity, player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forChunkInView(Player player, Location location, BiConsumer<Integer, Integer> func) {
|
private void addMissingViewers(REntity entity) {
|
||||||
int chunkX = posToChunk(location.getX());
|
int chunkX = posToChunk(entity.x());
|
||||||
int chunkZ = posToChunk(location.getZ());
|
int chunkZ = posToChunk(entity.z());
|
||||||
int viewDistance = this.viewDistance.get(player);
|
for (Player player : lastLocation.keySet()) {
|
||||||
|
if (inView(entity, player, chunkX, chunkZ) && entity.viewers.add(player)) {
|
||||||
for (int x = chunkX - viewDistance; x <= chunkX + viewDistance; x++) {
|
showTo(entity, player);
|
||||||
for (int z = chunkZ - viewDistance; z <= chunkZ + viewDistance; z++) {
|
|
||||||
func.accept(x, z);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPlayerToChunk(Player player, int x, int z) {
|
private boolean inView(REntity entity, Player player, int entityChunkX, int entityChunkZ) {
|
||||||
long id = chunkToId(x, z);
|
Location location = lastLocation.get(player);
|
||||||
players.computeIfAbsent(id, i -> new HashSet<>()).add(player);
|
int distance = Math.max(Math.abs(posToChunk(location.getX()) - entityChunkX), Math.abs(posToChunk(location.getZ()) - entityChunkZ));
|
||||||
for (REntity entity : entities.getOrDefault(id, emptyEntities)) {
|
return entity.canView(player, distance);
|
||||||
entity.list(packet -> TinyProtocol.instance.sendPacket(player, packet));
|
}
|
||||||
if (!entity.isHidden()) {
|
|
||||||
entity.spawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
|
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) {
|
private void hideFrom(REntity entity, Player player) {
|
||||||
long id = chunkToId(x, z);
|
if (!entity.isHidden()) {
|
||||||
|
entity.despawn(packet -> TinyProtocol.instance.sendPacket(player, packet));
|
||||||
Set<Player> 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));
|
|
||||||
}
|
}
|
||||||
|
entity.delist(packet -> TinyProtocol.instance.sendPacket(player, packet));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tick() {
|
public void tick() {
|
||||||
@@ -287,19 +255,10 @@ public class REntityServer implements Listener {
|
|||||||
return (int) (coord / 16) - (coord < 0 ? 1 : 0);
|
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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user