diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 98e54361b..604b93bfb 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -456,15 +456,14 @@ public final class MinecraftServer { final Collection players = connectionManager.getOnlinePlayers(); - UpdateViewDistancePacket updateViewDistancePacket = new UpdateViewDistancePacket(); - updateViewDistancePacket.viewDistance = chunkViewDistance; - - // Send packet to all online players - PacketUtils.sendGroupedPacket(players, updateViewDistancePacket); - players.forEach(player -> { final Chunk playerChunk = player.getChunk(); if (playerChunk != null) { + + UpdateViewDistancePacket updateViewDistancePacket = new UpdateViewDistancePacket(); + updateViewDistancePacket.viewDistance = player.getChunkRange(); + player.getPlayerConnection().sendPacket(updateViewDistancePacket); + player.refreshVisibleChunks(playerChunk); } }); diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index 4a548535c..2bd8b971f 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -28,6 +28,7 @@ import net.minestom.server.utils.Position; import net.minestom.server.utils.Vector; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.callback.OptionalCallback; +import net.minestom.server.utils.chunk.ChunkCallback; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.entity.EntityUtils; import net.minestom.server.utils.player.PlayerUtils; @@ -229,31 +230,36 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P * {@link Instance#hasEnabledAutoChunkLoad()} returns true. * * @param position the teleport position + * @param chunks the chunk indexes to load before teleporting the entity, + * indexes are from {@link ChunkUtils#getChunkIndex(int, int)}, + * can be null or empty to only load the chunk at {@code position} * @param callback the optional callback executed, even if auto chunk is not enabled + * @throws IllegalStateException if you try to teleport an entity before settings its instance */ - public void teleport(@NotNull Position position, @Nullable Runnable callback) { + public void teleport(@NotNull Position position, @Nullable long[] chunks, @Nullable Runnable callback) { Check.notNull(position, "Teleport position cannot be null"); Check.stateCondition(instance == null, "You need to use Entity#setInstance before teleporting an entity!"); - final Runnable runnable = () -> { - if (!this.position.isSimilar(position)) { - refreshPosition(position.getX(), position.getY(), position.getZ()); - } - if (!this.position.hasSimilarView(position)) { - refreshView(position.getYaw(), position.getPitch()); - } + final ChunkCallback endCallback = (chunk) -> { + refreshPosition(position.getX(), position.getY(), position.getZ()); + refreshView(position.getYaw(), position.getPitch()); + sendSynchronization(); OptionalCallback.execute(callback); }; - if (instance.hasEnabledAutoChunkLoad()) { - instance.loadChunk(position, chunk -> runnable.run()); + if (chunks == null || chunks.length == 0) { + instance.loadOptionalChunk(position, endCallback); } else { - runnable.run(); + ChunkUtils.optionalLoadAll(instance, chunks, null, endCallback); } } + public void teleport(@NotNull Position position, @Nullable Runnable callback) { + teleport(position, null, callback); + } + public void teleport(@NotNull Position position) { teleport(position, null); } @@ -1020,13 +1026,14 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P final Instance instance = getInstance(); if (instance != null) { + + // Needed to refresh the client chunks when connecting for the first time + final boolean forceUpdate = this instanceof Player && getAliveTicks() == 0; + final Chunk lastChunk = instance.getChunkAt(lastX, lastZ); final Chunk newChunk = instance.getChunkAt(x, z); - if (lastChunk != null && newChunk != null && lastChunk != newChunk) { - synchronized (instance) { - instance.removeEntityFromChunk(this, lastChunk); - instance.addEntityToChunk(this, newChunk); - } + if (forceUpdate || (lastChunk != null && newChunk != null && lastChunk != newChunk)) { + instance.switchEntityChunk(this, lastChunk, newChunk); if (this instanceof Player) { // Refresh player view final Player player = (Player) this; diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index 36c41bbee..2a21c93be 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -693,55 +693,42 @@ public class Player extends LivingEntity implements CommandSender { // true if the chunks need to be sent to the client, can be false if the instances share the same chunks (eg SharedInstance) final boolean needWorldRefresh = !InstanceUtils.areLinked(this.instance, instance); - // true if the player needs every chunk around its position - final boolean needChunkLoad = !firstSpawn || firstSpawn && - ChunkUtils.getChunkCoordinate((int) getRespawnPoint().getX()) == 0 && - ChunkUtils.getChunkCoordinate((int) getRespawnPoint().getZ()) == 0; - - if (needWorldRefresh && needChunkLoad) { + if (needWorldRefresh) { // Remove all previous viewable chunks (from the previous instance) for (Chunk viewableChunk : viewableChunks) { viewableChunk.removeViewer(this); } - + // Send the new dimension if (this.instance != null) { final DimensionType instanceDimensionType = instance.getDimensionType(); if (dimensionType != instanceDimensionType) sendDimension(instanceDimensionType); } + // Load all the required chunks + final Position pos = firstSpawn ? getRespawnPoint() : position; + final long[] visibleChunks = ChunkUtils.getChunksInRange(pos, getChunkRange()); - final long[] visibleChunks = ChunkUtils.getChunksInRange(position, getChunkRange()); - final int length = visibleChunks.length; - - AtomicInteger counter = new AtomicInteger(0); - for (long visibleChunk : visibleChunks) { - final int chunkX = ChunkUtils.getChunkCoordX(visibleChunk); - final int chunkZ = ChunkUtils.getChunkCoordZ(visibleChunk); - - final ChunkCallback callback = (chunk) -> { - if (chunk != null) { - chunk.addViewer(this); - if (chunk.getChunkX() == ChunkUtils.getChunkCoordinate((int) getPosition().getX()) && - chunk.getChunkZ() == ChunkUtils.getChunkCoordinate((int) getPosition().getZ())) { - updateViewPosition(chunk); - } + final ChunkCallback eachCallback = chunk -> { + if (chunk != null) { + final int chunkX = ChunkUtils.getChunkCoordinate((int) pos.getX()); + final int chunkZ = ChunkUtils.getChunkCoordinate((int) pos.getZ()); + if (chunk.getChunkX() == chunkX && + chunk.getChunkZ() == chunkZ) { + updateViewPosition(chunkX, chunkZ); } - final boolean isLast = counter.get() == length - 1; - if (isLast) { - // This is the last chunk to be loaded , spawn player - spawnPlayer(instance, firstSpawn); - } else { - // Increment the counter of current loaded chunks - counter.incrementAndGet(); - } - }; + } + }; + + final ChunkCallback endCallback = chunk -> { + // This is the last chunk to be loaded , spawn player + spawnPlayer(instance, firstSpawn); + }; + + // Chunk 0;0 always needs to be loaded + instance.loadChunk(0, 0, chunk -> + // Load all the required chunks + ChunkUtils.optionalLoadAll(instance, visibleChunks, eachCallback, endCallback)); - // WARNING: if auto load is disabled and no chunks are loaded beforehand, player will be stuck. - instance.loadOptionalChunk(chunkX, chunkZ, callback); - } - } else if (!needChunkLoad) { - // The player always believe that his position is 0;0 so this is a pretty hacky fix - instance.loadOptionalChunk(0, 0, chunk -> spawnPlayer(instance, true)); } else { // The player already has the good version of all the chunks. // We just need to refresh his entity viewing list and add him to the instance @@ -758,7 +745,7 @@ public class Player extends LivingEntity implements CommandSender { * * @param firstSpawn true if this is the player first spawn */ - private void spawnPlayer(Instance instance, boolean firstSpawn) { + private void spawnPlayer(@NotNull Instance instance, boolean firstSpawn) { this.viewableEntities.forEach(entity -> entity.removeViewer(this)); super.setInstance(instance); @@ -767,7 +754,6 @@ public class Player extends LivingEntity implements CommandSender { teleport(getRespawnPoint()); } - PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn); callEvent(PlayerSpawnEvent.class, spawnEvent); } @@ -1545,7 +1531,7 @@ public class Player extends LivingEntity implements CommandSender { } // Update client render distance - updateViewPosition(newChunk); + updateViewPosition(newChunk.getChunkX(), newChunk.getChunkZ()); // Load new chunks for (int index : newChunks) { @@ -1610,13 +1596,19 @@ public class Player extends LivingEntity implements CommandSender { } @Override - public void teleport(@NotNull Position position, @Nullable Runnable callback) { - super.teleport(position, () -> { + public void teleport(@NotNull Position position, @Nullable long[] chunks, @Nullable Runnable callback) { + super.teleport(position, chunks, () -> { updatePlayerPosition(); OptionalCallback.execute(callback); }); } + @Override + public void teleport(@NotNull Position position, @Nullable Runnable callback) { + final long[] chunks = ChunkUtils.getChunksInRange(position, getChunkRange()); + teleport(position, chunks, callback); + } + @Override public void teleport(@NotNull Position position) { teleport(position, null); @@ -1948,12 +1940,13 @@ public class Player extends LivingEntity implements CommandSender { /** * Sends a {@link UpdateViewPositionPacket} to the player. * - * @param chunk the chunk to update the view + * @param chunkX the chunk X + * @param chunkZ the chunk Z */ - public void updateViewPosition(@NotNull Chunk chunk) { + public void updateViewPosition(int chunkX, int chunkZ) { UpdateViewPositionPacket updateViewPositionPacket = new UpdateViewPositionPacket(); - updateViewPositionPacket.chunkX = chunk.getChunkX(); - updateViewPositionPacket.chunkZ = chunk.getChunkZ(); + updateViewPositionPacket.chunkX = chunkX; + updateViewPositionPacket.chunkZ = chunkZ; playerConnection.sendPacket(updateViewPositionPacket); } diff --git a/src/main/java/net/minestom/server/extras/mojangAuth/Encrypter.java b/src/main/java/net/minestom/server/extras/mojangAuth/Encrypter.java index 8c1fba174..053ca36cb 100644 --- a/src/main/java/net/minestom/server/extras/mojangAuth/Encrypter.java +++ b/src/main/java/net/minestom/server/extras/mojangAuth/Encrypter.java @@ -7,13 +7,13 @@ import io.netty.handler.codec.MessageToByteEncoder; import javax.crypto.Cipher; public class Encrypter extends MessageToByteEncoder { - private final CipherBase cipher; + private final CipherBase cipher; - public Encrypter(Cipher cipher) { - this.cipher = new CipherBase(cipher); - } + public Encrypter(Cipher cipher) { + this.cipher = new CipherBase(cipher); + } - protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn, ByteBuf byteBufOut) throws Exception { - this.cipher.encrypt(byteBufIn, byteBufOut); - } + protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn, ByteBuf byteBufOut) throws Exception { + this.cipher.encrypt(byteBufIn, byteBufOut); + } } diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index ce068d7e5..c1319ba80 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -409,13 +409,17 @@ public abstract class Chunk implements Viewable, DataContainer { public boolean addViewer(@NotNull Player player) { final boolean result = this.viewers.add(player); - // Send the chunk data & light packets to the player - sendChunk(player); // Add to the viewable chunks set player.getViewableChunks().add(this); - PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ); - player.callEvent(PlayerChunkLoadEvent.class, playerChunkLoadEvent); + if (result) { + // Send the chunk data & light packets to the player + sendChunk(player); + + PlayerChunkLoadEvent playerChunkLoadEvent = new PlayerChunkLoadEvent(player, chunkX, chunkZ); + player.callEvent(PlayerChunkLoadEvent.class, playerChunkLoadEvent); + } + return result; } @@ -433,8 +437,11 @@ public abstract class Chunk implements Viewable, DataContainer { // Remove from the viewable chunks set player.getViewableChunks().remove(this); - PlayerChunkUnloadEvent playerChunkUnloadEvent = new PlayerChunkUnloadEvent(player, chunkX, chunkZ); - player.callEvent(PlayerChunkUnloadEvent.class, playerChunkUnloadEvent); + if (result) { + PlayerChunkUnloadEvent playerChunkUnloadEvent = new PlayerChunkUnloadEvent(player, chunkX, chunkZ); + player.callEvent(PlayerChunkUnloadEvent.class, playerChunkUnloadEvent); + } + return result; } @@ -464,9 +471,6 @@ public abstract class Chunk implements Viewable, DataContainer { // Only send loaded chunk if (!isLoaded()) return; - // Only send chunk to netty client (because it sends raw ByteBuf buffer) - if (!PlayerUtils.isNettyClient(player)) - return; final PlayerConnection playerConnection = player.getPlayerConnection(); diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 524041707..37e1b11c9 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -26,9 +26,7 @@ import net.minestom.server.world.biomes.Biome; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.HashSet; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; /** * Represents a {@link Chunk} which store each individual block in memory. @@ -55,7 +53,7 @@ public class DynamicChunk extends Chunk { protected final Int2LongMap updatableBlocksLastUpdate = new Int2LongOpenHashMap(); // Block entities - protected final Set blockEntities = new CopyOnWriteArraySet<>(); + protected final IntSet blockEntities = new IntOpenHashSet(); private long lastChangeTime; @@ -384,12 +382,12 @@ public class DynamicChunk extends Chunk { @Override protected ChunkDataPacket createFreshPacket() { ChunkDataPacket fullDataPacket = new ChunkDataPacket(getIdentifier(), getLastChangeTime()); - fullDataPacket.biomes = biomes.clone(); + fullDataPacket.biomes = biomes; fullDataPacket.chunkX = chunkX; fullDataPacket.chunkZ = chunkZ; fullDataPacket.paletteStorage = blockPalette.copy(); fullDataPacket.customBlockPaletteStorage = customBlockPalette.copy(); - fullDataPacket.blockEntities = new HashSet<>(blockEntities); + fullDataPacket.blockEntities = new IntOpenHashSet(blockEntities); fullDataPacket.blocksData = new Int2ObjectOpenHashMap<>(blocksData); return fullDataPacket; } diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index 5e6cf3fac..80cf4a900 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -893,6 +893,19 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta }); } + /** + * Synchronized method to execute {@link #removeEntityFromChunk(Entity, Chunk)} + * and {@link #addEntityToChunk(Entity, Chunk)} simultaneously. + * + * @param entity the entity to change its chunk + * @param lastChunk the last entity chunk + * @param newChunk the new entity chunk + */ + public synchronized void switchEntityChunk(@NotNull Entity entity, @NotNull Chunk lastChunk, @NotNull Chunk newChunk) { + removeEntityFromChunk(entity, lastChunk); + addEntityToChunk(entity, newChunk); + } + /** * Adds the specified {@link Entity} to the instance entities cache. *

diff --git a/src/main/java/net/minestom/server/network/packet/server/play/ChunkDataPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/ChunkDataPacket.java index 1bb6e5f6b..845247cca 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/ChunkDataPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/ChunkDataPacket.java @@ -2,6 +2,7 @@ package net.minestom.server.network.packet.server.play; import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.IntSet; import net.minestom.server.MinecraftServer; import net.minestom.server.data.Data; import net.minestom.server.instance.block.BlockManager; @@ -21,7 +22,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jglrxavpok.hephaistos.nbt.NBTCompound; -import java.util.Set; import java.util.UUID; public class ChunkDataPacket implements ServerPacket, CacheablePacket { @@ -36,7 +36,7 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket { public PaletteStorage paletteStorage; public PaletteStorage customBlockPaletteStorage; - public Set blockEntities; + public IntSet blockEntities; public Int2ObjectMap blocksData; public int[] sections; diff --git a/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java b/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java index aaf8c8434..0dc29e9ca 100644 --- a/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java @@ -80,9 +80,9 @@ public class NettyPlayerConnection extends PlayerConnection { public void setEncryptionKey(@NotNull SecretKey secretKey) { Check.stateCondition(encrypted, "Encryption is already enabled!"); this.encrypted = true; - channel.pipeline().addBefore(NettyServer.FRAMER_HANDLER_NAME, NettyServer.DECRYPT_HANDLER_NAME, + channel.pipeline().addBefore(NettyServer.GROUPED_PACKET_HANDLER_NAME, NettyServer.DECRYPT_HANDLER_NAME, new Decrypter(MojangCrypt.getCipher(2, secretKey))); - channel.pipeline().addBefore(NettyServer.FRAMER_HANDLER_NAME, NettyServer.ENCRYPT_HANDLER_NAME, + channel.pipeline().addBefore(NettyServer.GROUPED_PACKET_HANDLER_NAME, NettyServer.ENCRYPT_HANDLER_NAME, new Encrypter(MojangCrypt.getCipher(1, secretKey))); } diff --git a/src/main/java/net/minestom/server/utils/binary/BinaryReader.java b/src/main/java/net/minestom/server/utils/binary/BinaryReader.java index d4ba4711b..3db862cca 100644 --- a/src/main/java/net/minestom/server/utils/binary/BinaryReader.java +++ b/src/main/java/net/minestom/server/utils/binary/BinaryReader.java @@ -90,7 +90,7 @@ public class BinaryReader extends InputStream { */ public String readSizedString(int maxLength) { final int length = readVarInt(); - Check.stateCondition(length >= maxLength, + Check.stateCondition(length > maxLength, "String length (" + length + ") was higher than the max length of " + maxLength); final byte[] bytes = readBytes(length); diff --git a/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java b/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java index 7aae69acb..59b68c669 100644 --- a/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java +++ b/src/main/java/net/minestom/server/utils/chunk/ChunkUtils.java @@ -7,15 +7,57 @@ import net.minestom.server.instance.Instance; import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.Position; +import net.minestom.server.utils.callback.OptionalCallback; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.concurrent.atomic.AtomicInteger; + public final class ChunkUtils { private ChunkUtils() { } + /** + * Executes {@link Instance#loadOptionalChunk(int, int, ChunkCallback)} for the array of chunks {@code chunks} + * with multiple callbacks, {@code eachCallback} which is executed each time a new chunk is loaded and + * {@code endCallback} when all the chunks in the array have been loaded. + *

+ * Be aware that {@link Instance#loadOptionalChunk(int, int, ChunkCallback)} can give a null chunk in the callback + * if {@link Instance#hasEnabledAutoChunkLoad()} returns false and the chunk is not already loaded. + * + * @param instance the instance to load the chunks from + * @param chunks the chunks to loaded, long value from {@link #getChunkIndex(int, int)} + * @param eachCallback the optional callback when a chunk get loaded + * @param endCallback the optional callback when all the chunks have been loaded + */ + public static void optionalLoadAll(@NotNull Instance instance, @NotNull long[] chunks, + @Nullable ChunkCallback eachCallback, @Nullable ChunkCallback endCallback) { + final int length = chunks.length; + AtomicInteger counter = new AtomicInteger(0); + + for (long visibleChunk : chunks) { + final int chunkX = ChunkUtils.getChunkCoordX(visibleChunk); + final int chunkZ = ChunkUtils.getChunkCoordZ(visibleChunk); + + final ChunkCallback callback = (chunk) -> { + OptionalCallback.execute(eachCallback, chunk); + final boolean isLast = counter.get() == length - 1; + if (isLast) { + // This is the last chunk to be loaded , spawn player + OptionalCallback.execute(endCallback, chunk); + } else { + // Increment the counter of current loaded chunks + counter.incrementAndGet(); + } + }; + + // WARNING: if auto load is disabled and no chunks are loaded beforehand, player will be stuck. + instance.loadOptionalChunk(chunkX, chunkZ, callback); + } + } + /** * Gets if a chunk is loaded. *