diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index 71086a059..0db074524 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -49,7 +49,7 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, private boolean readOnly; protected volatile boolean loaded = true; - private final ChunkView viewers; + private final Viewable viewable; // Path finding protected PFColumnarSpace columnarSpace; @@ -65,7 +65,9 @@ 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 = new ChunkView(instance, toPosition()); + final List shared = instance instanceof InstanceContainer instanceContainer ? + instanceContainer.getSharedInstances() : List.of(); + this.viewable = instance.getEntityTracker().viewable(shared, chunkX, chunkZ); } /** @@ -267,17 +269,17 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, @Override public boolean addViewer(@NotNull Player player) { - throw new UnsupportedOperationException("Chunk does not support manual viewers"); + return viewable.addViewer(player); } @Override public boolean removeViewer(@NotNull Player player) { - throw new UnsupportedOperationException("Chunk does not support manual viewers"); + return viewable.removeViewer(player); } @Override public @NotNull Set getViewers() { - return viewers.set; + return viewable.getViewers(); } @Override diff --git a/src/main/java/net/minestom/server/instance/ChunkView.java b/src/main/java/net/minestom/server/instance/ChunkView.java deleted file mode 100644 index d7dfc93d1..000000000 --- a/src/main/java/net/minestom/server/instance/ChunkView.java +++ /dev/null @@ -1,57 +0,0 @@ -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 final Instance instance; - private final Point point; - final Set set = new SetImpl(); - - private int lastReferenceCount; - - ChunkView(Instance instance, Point point) { - this.instance = instance; - this.point = point; - } - - private Collection references() { - Int2ObjectOpenHashMap entityMap = new Int2ObjectOpenHashMap<>(lastReferenceCount); - collectPlayers(instance, entityMap); - if (instance instanceof InstanceContainer container && !container.getSharedInstances().isEmpty()) { - for (Instance shared : container.getSharedInstances()) { - collectPlayers(shared, entityMap); - } - } - this.lastReferenceCount = entityMap.size(); - return entityMap.values(); - } - - private void collectPlayers(Instance instance, Int2ObjectOpenHashMap map) { - instance.getEntityTracker().nearbyEntitiesByChunkRange(point, MinecraftServer.getChunkViewDistance(), - EntityTracker.Target.PLAYERS, (player) -> map.putIfAbsent(player.getEntityId(), player)); - } - - final class SetImpl extends AbstractSet { - @Override - public @NotNull Iterator iterator() { - return references().iterator(); - } - - @Override - public int size() { - return references().size(); - } - - @Override - public void forEach(Consumer action) { - references().forEach(action); - } - } -} diff --git a/src/main/java/net/minestom/server/instance/EntityTracker.java b/src/main/java/net/minestom/server/instance/EntityTracker.java index d07832877..0ca789ebd 100644 --- a/src/main/java/net/minestom/server/instance/EntityTracker.java +++ b/src/main/java/net/minestom/server/instance/EntityTracker.java @@ -1,5 +1,6 @@ package net.minestom.server.instance; +import net.minestom.server.Viewable; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Entity; import net.minestom.server.entity.ExperienceOrb; @@ -55,7 +56,7 @@ public sealed interface EntityTracker permits EntityTrackerImpl { * Gets the entities within a chunk range. */ void nearbyEntitiesByChunkRange(@NotNull Point point, int chunkRange, - @NotNull Target target, @NotNull Consumer query); + @NotNull Target target, @NotNull Consumer query); /** * Gets the entities within a range. @@ -74,6 +75,12 @@ public sealed interface EntityTracker permits EntityTrackerImpl { return entities(Target.ENTITIES); } + @NotNull Viewable viewable(@NotNull List<@NotNull SharedInstance> sharedInstances, int chunkX, int chunkZ); + + default @NotNull Viewable viewable(int chunkX, int chunkZ) { + return viewable(List.of(), chunkX, chunkZ); + } + /** * Represents the type of entity you want to retrieve. * diff --git a/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java b/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java index 49ad02e7f..bf61bf1a2 100644 --- a/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java +++ b/src/main/java/net/minestom/server/instance/EntityTrackerImpl.java @@ -1,8 +1,12 @@ package net.minestom.server.instance; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.minestom.server.MinecraftServer; +import net.minestom.server.Viewable; import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; +import net.minestom.server.entity.Player; import net.minestom.server.utils.chunk.ChunkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,16 +15,15 @@ import org.jetbrains.annotations.UnmodifiableView; import space.vectrix.flare.fastutil.Int2ObjectSyncMap; import space.vectrix.flare.fastutil.Long2ObjectSyncMap; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; +import static net.minestom.server.instance.Chunk.CHUNK_SIZE_X; +import static net.minestom.server.instance.Chunk.CHUNK_SIZE_Z; import static net.minestom.server.utils.chunk.ChunkUtils.*; final class EntityTrackerImpl implements EntityTracker { @@ -168,6 +171,12 @@ final class EntityTrackerImpl implements EntityTracker { return (Set) entries[target.ordinal()].entitiesView; } + @Override + public @NotNull Viewable viewable(@NotNull List<@NotNull SharedInstance> sharedInstances, int chunkX, int chunkZ) { + var entry = entries[Target.PLAYERS.ordinal()]; + return entry.viewers.computeIfAbsent(new ChunkViewKey(sharedInstances, chunkX, chunkZ), ChunkView::new); + } + private void difference(Point oldPoint, Point newPoint, @NotNull Target target, @NotNull Update update) { final TargetEntry entry = entries[target.ordinal()]; @@ -185,12 +194,24 @@ final class EntityTrackerImpl implements EntityTracker { }); } + record ChunkViewKey(List sharedInstances, int chunkX, int chunkZ) { + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ChunkViewKey key)) return false; + return sharedInstances == key.sharedInstances && + chunkX == key.chunkX && + chunkZ == key.chunkZ; + } + } + static final class TargetEntry { private final EntityTracker.Target target; private final Set entities = ConcurrentHashMap.newKeySet(); // Thread-safe since exposed private final Set entitiesView = Collections.unmodifiableSet(entities); // Chunk index -> entities inside it final Long2ObjectSyncMap> chunkEntities = Long2ObjectSyncMap.hashmap(); + final Map viewers = new ConcurrentHashMap<>(); TargetEntry(Target target) { this.target = target; @@ -209,4 +230,70 @@ final class EntityTrackerImpl implements EntityTracker { if (entities != null) entities.remove(entity); } } + + private final class ChunkView implements Viewable { + private final ChunkViewKey key; + private final int chunkX, chunkZ; + private final Point point; + final Set set = new SetImpl(); + private int lastReferenceCount; + + private ChunkView(ChunkViewKey key) { + this.key = key; + + this.chunkX = key.chunkX; + this.chunkZ = key.chunkZ; + + this.point = new Vec(CHUNK_SIZE_X * chunkX, 0, CHUNK_SIZE_Z * chunkZ); + } + + @Override + public boolean addViewer(@NotNull Player player) { + throw new UnsupportedOperationException("Chunk does not support manual viewers"); + } + + @Override + public boolean removeViewer(@NotNull Player player) { + throw new UnsupportedOperationException("Chunk does not support manual viewers"); + } + + @Override + public @NotNull Set<@NotNull Player> getViewers() { + return set; + } + + private Collection references() { + Int2ObjectOpenHashMap entityMap = new Int2ObjectOpenHashMap<>(lastReferenceCount); + collectPlayers(EntityTrackerImpl.this, entityMap); + if (!key.sharedInstances.isEmpty()) { + for (SharedInstance instance : key.sharedInstances) { + collectPlayers(instance.getEntityTracker(), entityMap); + } + } + this.lastReferenceCount = entityMap.size(); + return entityMap.values(); + } + + private void collectPlayers(EntityTracker tracker, Int2ObjectOpenHashMap map) { + tracker.nearbyEntitiesByChunkRange(point, MinecraftServer.getChunkViewDistance(), + EntityTracker.Target.PLAYERS, (player) -> map.putIfAbsent(player.getEntityId(), player)); + } + + final class SetImpl extends AbstractSet { + @Override + public @NotNull Iterator iterator() { + return references().iterator(); + } + + @Override + public int size() { + return references().size(); + } + + @Override + public void forEach(Consumer action) { + references().forEach(action); + } + } + } } diff --git a/src/test/java/net/minestom/server/instance/ChunkViewerIntegrationTest.java b/src/test/java/net/minestom/server/instance/ChunkViewerIntegrationTest.java index e64819127..429a26a0a 100644 --- a/src/test/java/net/minestom/server/instance/ChunkViewerIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/ChunkViewerIntegrationTest.java @@ -1,11 +1,11 @@ package net.minestom.server.instance; import net.minestom.server.MinecraftServer; -import net.minestom.testing.Env; -import net.minestom.testing.EnvTest; import net.minestom.server.coordinate.Pos; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -30,7 +30,8 @@ public class ChunkViewerIntegrationTest { assertEquals(0, chunk.getViewers().size()); var player = env.createPlayer(instance, new Pos(0, 40, 0)); - assertEquals(1, chunk.getViewers().size()); + assertEquals(1, chunk.getViewers().size(), sharedInstance ? + "Chunk viewer set must include players from shared instance" : "Instance should have 1 viewer"); assertEquals(player, chunk.getViewers().iterator().next()); } diff --git a/src/test/java/net/minestom/server/instance/EntityTrackerIntegrationTest.java b/src/test/java/net/minestom/server/instance/EntityTrackerIntegrationTest.java index 4896e30ac..23c78ab95 100644 --- a/src/test/java/net/minestom/server/instance/EntityTrackerIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/EntityTrackerIntegrationTest.java @@ -1,14 +1,14 @@ package net.minestom.server.instance; import net.minestom.server.MinecraftServer; -import net.minestom.testing.Env; -import net.minestom.testing.EnvTest; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.Player; import net.minestom.server.network.packet.server.SendablePacket; import net.minestom.server.network.player.PlayerConnection; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; @@ -17,6 +17,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; @EnvTest public class EntityTrackerIntegrationTest { @@ -35,6 +36,7 @@ public class EntityTrackerIntegrationTest { public void updateNewViewer(Player player) { viewersCount.incrementAndGet(); } + @Override public void updateOldViewer(Player player) { viewersCount.decrementAndGet(); @@ -66,6 +68,7 @@ public class EntityTrackerIntegrationTest { public void updateNewViewer(Player player) { viewersCount.incrementAndGet(); } + @Override public void updateOldViewer(Player player) { viewersCount.decrementAndGet(); @@ -83,6 +86,49 @@ public class EntityTrackerIntegrationTest { assertEquals(1, viewersCount.get()); } + @Test + public void viewable(Env env) { + final Instance instance = env.createFlatInstance(); + final Pos spawnPos = new Pos(0, 41, 0); + var viewable = instance.getEntityTracker().viewable(spawnPos.chunkX(), spawnPos.chunkZ()); + assertEquals(0, viewable.getViewers().size()); + + final Player player = env.createPlayer(instance, spawnPos); + assertEquals(1, viewable.getViewers().size()); + assertSame(viewable, instance.getEntityTracker().viewable(spawnPos.chunkX(), spawnPos.chunkZ())); + + player.teleport(new Pos(10_000, 41, 0)).join(); + assertEquals(0, viewable.getViewers().size()); + + player.teleport(spawnPos).join(); + assertEquals(1, viewable.getViewers().size()); + } + + @Test + public void viewableShared(Env env) { + final InstanceContainer instance = (InstanceContainer) env.createFlatInstance(); + var shared = env.process().instance().createSharedInstance(instance); + var sharedList = instance.getSharedInstances(); + + final Pos spawnPos = new Pos(0, 41, 0); + var viewable = instance.getEntityTracker().viewable(sharedList, spawnPos.chunkX(), spawnPos.chunkZ()); + assertEquals(0, viewable.getViewers().size()); + + final Player player = env.createPlayer(instance, spawnPos); + assertEquals(1, viewable.getViewers().size()); + assertSame(viewable, instance.getEntityTracker().viewable(sharedList, spawnPos.chunkX(), spawnPos.chunkZ())); + + player.setInstance(shared).join(); + assertEquals(1, viewable.getViewers().size()); + + player.teleport(new Pos(10_000, 41, 0)).join(); + assertEquals(0, viewable.getViewers().size()); + + var shared2 = env.process().instance().createSharedInstance(instance); + player.setInstance(shared2, spawnPos).join(); + assertEquals(1, viewable.getViewers().size()); + } + private Player createTestPlayer() { return new Player(UUID.randomUUID(), "TestPlayer", new PlayerConnection() { @Override