View engine improvements (#715)

This commit is contained in:
TheMode 2022-02-26 00:10:26 +01:00 committed by GitHub
parent 487fbcb5b9
commit 50c0f01fb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 157 deletions

View File

@ -40,7 +40,6 @@ import net.minestom.server.timer.Schedulable;
import net.minestom.server.timer.Scheduler;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.ViewEngine;
import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.block.BlockIterator;
import net.minestom.server.utils.chunk.ChunkUtils;
@ -135,36 +134,8 @@ public class Entity implements Viewable, Tickable, Schedulable, TagHandler, Perm
}
};
protected final ViewEngine viewEngine = new ViewEngine(this,
player -> {
// Add viewable
var lock1 = player.getEntityId() < getEntityId() ? player : this;
var lock2 = lock1 == this ? player : this;
synchronized (lock1.viewEngine.mutex()) {
synchronized (lock2.viewEngine.mutex()) {
if (!Entity.this.viewEngine.viewableOption.predicate(player) ||
!player.viewEngine.viewerOption.predicate(this)) return;
Entity.this.viewEngine.viewableOption.register(player);
player.viewEngine.viewerOption.register(this);
}
}
updateNewViewer(player);
},
player -> {
// Remove viewable
var lock1 = player.getEntityId() < getEntityId() ? player : this;
var lock2 = lock1 == this ? player : this;
synchronized (lock1.viewEngine.mutex()) {
synchronized (lock2.viewEngine.mutex()) {
Entity.this.viewEngine.viewableOption.unregister(player);
player.viewEngine.viewerOption.unregister(this);
}
}
updateOldViewer(player);
},
this instanceof Player player ? entity -> entity.viewEngine.viewableOption.addition.accept(player) : null,
this instanceof Player player ? entity -> entity.viewEngine.viewableOption.removal.accept(player) : null);
protected final Set<Player> viewers = viewEngine.asSet();
protected final EntityView viewEngine = new EntityView(this);
protected final Set<Player> viewers = viewEngine.set;
private final MutableNBTCompound nbtCompound = new MutableNBTCompound();
private final Scheduler scheduler = Scheduler.newScheduler();
private final Set<Permission> permissions = new CopyOnWriteArraySet<>();

View File

@ -1,17 +1,14 @@
package net.minestom.server.utils;
package net.minestom.server.entity;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.EntityTracker;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.SharedInstance;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -20,13 +17,9 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* Defines which players are able to see this element.
*/
@ApiStatus.Internal
public final class ViewEngine {
final class EntityView {
private static final int RANGE = MinecraftServer.getEntityViewDistance() * 16;
private final Entity entity;
private final int range;
private final Set<Player> manualViewers = new HashSet<>();
// Decide if this entity should be viewable to X players
@ -34,32 +27,42 @@ public final class ViewEngine {
// Decide if this entity should view X entities
public final Option<Entity> viewerOption;
private final Set<Player> set;
final Set<Player> set = new SetImpl();
private final Object mutex = this;
private volatile TrackedLocation trackedLocation;
public ViewEngine(@Nullable Entity entity,
Consumer<Player> autoViewableAddition, Consumer<Player> autoViewableRemoval,
Consumer<Entity> autoViewerAddition, Consumer<Entity> autoViewerRemoval) {
public EntityView(Entity entity) {
this.entity = entity;
if (entity != null) {
this.range = MinecraftServer.getEntityViewDistance() * 16;
this.set = new EntitySet();
} else {
this.range = MinecraftServer.getChunkViewDistance() * 16;
this.set = new ChunkSet();
}
this.viewableOption = new Option<>(EntityTracker.Target.PLAYERS, Entity::autoViewEntities, autoViewableAddition, autoViewableRemoval);
this.viewerOption = new Option<>(EntityTracker.Target.ENTITIES, Entity::isAutoViewable, autoViewerAddition, autoViewerRemoval);
}
public ViewEngine(@Nullable Entity entity) {
this(entity, null, null, null, null);
}
public ViewEngine() {
this(null);
this.viewableOption = new Option<>(EntityTracker.Target.PLAYERS, Entity::autoViewEntities,
player -> {
// Add viewable
var lock1 = player.getEntityId() < entity.getEntityId() ? player : entity;
var lock2 = lock1 == entity ? player : entity;
synchronized (lock1.viewEngine.mutex) {
synchronized (lock2.viewEngine.mutex) {
if (!entity.viewEngine.viewableOption.predicate(player) ||
!player.viewEngine.viewerOption.predicate(entity)) return;
entity.viewEngine.viewableOption.register(player);
player.viewEngine.viewerOption.register(entity);
}
}
entity.updateNewViewer(player);
}, player -> {
// Remove viewable
var lock1 = player.getEntityId() < entity.getEntityId() ? player : entity;
var lock2 = lock1 == entity ? player : entity;
synchronized (lock1.viewEngine.mutex) {
synchronized (lock2.viewEngine.mutex) {
entity.viewEngine.viewableOption.unregister(player);
player.viewEngine.viewerOption.unregister(entity);
}
}
entity.updateOldViewer(player);
});
this.viewerOption = new Option<>(EntityTracker.Target.ENTITIES, Entity::isAutoViewable,
entity instanceof Player player ? e -> e.viewEngine.viewableOption.addition.accept(player) : null,
entity instanceof Player player ? e -> e.viewEngine.viewableOption.removal.accept(player) : null);
}
public void updateTracker(@Nullable Instance instance, @NotNull Point point) {
@ -124,17 +127,9 @@ public final class ViewEngine {
}
}
public Object mutex() {
return mutex;
}
public Set<Player> asSet() {
return set;
}
public final class Option<T extends Entity> {
@SuppressWarnings("rawtypes")
private static final AtomicIntegerFieldUpdater<Option> UPDATER = AtomicIntegerFieldUpdater.newUpdater(Option.class, "auto");
private static final AtomicIntegerFieldUpdater<EntityView.Option> UPDATER = AtomicIntegerFieldUpdater.newUpdater(EntityView.Option.class, "auto");
// Entities that should be tracked from this option
private final EntityTracker.Target<T> target;
// The condition that must be met for this option to be considered auto.
@ -212,7 +207,7 @@ public final class ViewEngine {
private void update(Predicate<T> visibilityPredicate,
Consumer<T> action) {
references().forEach(entity -> {
if (entity == ViewEngine.this.entity || !visibilityPredicate.test(entity)) return;
if (entity == EntityView.this.entity || !visibilityPredicate.test(entity)) return;
if (entity instanceof Player player && manualViewers.contains(player)) return;
if (entity.getVehicle() != null) return;
action.accept(entity);
@ -222,21 +217,21 @@ public final class ViewEngine {
private int lastSize;
private Collection<T> references() {
final TrackedLocation trackedLocation = ViewEngine.this.trackedLocation;
final TrackedLocation trackedLocation = EntityView.this.trackedLocation;
if (trackedLocation == null) return List.of();
final Instance instance = trackedLocation.instance();
final Point point = trackedLocation.point();
Int2ObjectOpenHashMap<T> entityMap = new Int2ObjectOpenHashMap<>(lastSize);
// Current Instance
instance.getEntityTracker().nearbyEntities(point, range, target,
instance.getEntityTracker().nearbyEntities(point, RANGE, target,
(entity) -> entityMap.putIfAbsent(entity.getEntityId(), entity));
// Shared Instances
if (instance instanceof InstanceContainer container) {
final List<SharedInstance> shared = container.getSharedInstances();
if (!shared.isEmpty()) {
for (var sharedInstance : shared) {
sharedInstance.getEntityTracker().nearbyEntities(point, range, target,
sharedInstance.getEntityTracker().nearbyEntities(point, RANGE, target,
(entity) -> entityMap.putIfAbsent(entity.getEntityId(), entity));
}
}
@ -246,24 +241,7 @@ public final class ViewEngine {
}
}
final class ChunkSet extends AbstractSet<Player> {
@Override
public @NotNull Iterator<Player> iterator() {
return viewableOption.references().iterator();
}
@Override
public int size() {
return viewableOption.references().size();
}
@Override
public void forEach(Consumer<? super Player> action) {
viewableOption.references().forEach(action);
}
}
final class EntitySet extends AbstractSet<Player> {
final class SetImpl extends AbstractSet<Player> {
@Override
public @NotNull Iterator<Player> iterator() {
synchronized (mutex) {

View File

@ -10,7 +10,6 @@ import net.minestom.server.instance.block.Block;
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.utils.ViewEngine;
import net.minestom.server.utils.chunk.ChunkSupplier;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.world.biomes.Biome;
@ -51,7 +50,7 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter,
private boolean readOnly;
protected volatile boolean loaded = true;
private final ViewEngine viewers = new ViewEngine();
private final ChunkView viewers;
// Path finding
protected PFColumnarSpace columnarSpace;
@ -67,8 +66,7 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter,
this.shouldGenerate = shouldGenerate;
this.minSection = instance.getDimensionType().getMinY() / CHUNK_SECTION_SIZE;
this.maxSection = (instance.getDimensionType().getMinY() + instance.getDimensionType().getHeight()) / CHUNK_SECTION_SIZE;
this.viewers.updateTracker(instance, toPosition());
this.viewers = new ChunkView(instance, toPosition());
}
/**
@ -268,31 +266,19 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter,
return getClass().getSimpleName() + "[" + chunkX + ":" + chunkZ + "]";
}
/**
* Adds the player to the viewing collection. Chunk packet must be sent manually.
*
* @param player the viewer to add
* @return true if the player has just been added to the viewer collection
*/
@Override
public boolean addViewer(@NotNull Player player) {
return viewers.manualAdd(player);
throw new UnsupportedOperationException("Chunk does not support manual viewers");
}
/**
* Removes the player from the viewing collection. Chunk packet must be sent manually.
*
* @param player the viewer to remove
* @return true if the player has just been removed to the viewer collection
*/
@Override
public boolean removeViewer(@NotNull Player player) {
return viewers.manualRemove(player);
throw new UnsupportedOperationException("Chunk does not support manual viewers");
}
@Override
public @NotNull Set<Player> getViewers() {
return viewers.asSet();
return viewers.set;
}
@Override

View File

@ -0,0 +1,64 @@
package net.minestom.server.instance;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.Consumer;
final class ChunkView {
private static final int RANGE = MinecraftServer.getChunkViewDistance() * 16;
private final Instance instance;
private final Point point;
final Set<Player> set = new SetImpl();
private int lastReferenceCount;
ChunkView(Instance instance, Point point) {
this.instance = instance;
this.point = point;
}
private Collection<Player> references() {
final Instance instance = this.instance;
final Point point = this.point;
final var target = EntityTracker.Target.PLAYERS;
Int2ObjectOpenHashMap<Player> entityMap = new Int2ObjectOpenHashMap<>(lastReferenceCount);
// Current Instance
instance.getEntityTracker().nearbyEntities(point, RANGE, target,
(entity) -> entityMap.putIfAbsent(entity.getEntityId(), entity));
// Shared Instances
if (instance instanceof InstanceContainer container) {
final List<SharedInstance> shared = container.getSharedInstances();
if (!shared.isEmpty()) {
for (var sharedInstance : shared) {
sharedInstance.getEntityTracker().nearbyEntities(point, RANGE, target,
(entity) -> entityMap.putIfAbsent(entity.getEntityId(), entity));
}
}
}
this.lastReferenceCount = entityMap.size();
return entityMap.values();
}
final class SetImpl extends AbstractSet<Player> {
@Override
public @NotNull Iterator<Player> iterator() {
return references().iterator();
}
@Override
public int size() {
return references().size();
}
@Override
public void forEach(Consumer<? super Player> action) {
references().forEach(action);
}
}
}

View File

@ -1,45 +0,0 @@
package net.minestom.server.utils;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
public class ViewEngineTest {
@Test
public void empty() {
ViewEngine viewEngine = new ViewEngine();
var set = viewEngine.asSet();
assertEquals(0, set.size());
// forEach
set.forEach(player -> fail("The engine should have no entity"));
// Iterator
var iterator = set.iterator();
assertFalse(iterator.hasNext());
// Array
assertArrayEquals(new Entity[0], set.toArray());
}
@Test
public void playerEmpty() {
Player owner = createPlayer();
ViewEngine viewEngine = new ViewEngine(owner);
var set = viewEngine.asSet();
assertEquals(0, set.size());
// forEach
set.forEach(player -> fail("The engine should have no entity"));
// Iterator
var iterator = set.iterator();
assertFalse(iterator.hasNext());
// Array
assertArrayEquals(new Entity[0], set.toArray());
}
static Player createPlayer() {
return new Player(UUID.randomUUID(), "test", null);
}
}