diff --git a/.github/workflows/check-pr-style.yml b/.github/workflows/check-pr-style.yml new file mode 100644 index 000000000..7ac69d24b --- /dev/null +++ b/.github/workflows/check-pr-style.yml @@ -0,0 +1,31 @@ +name: Check PR code style + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 1.11 + - name: Run java checkstyle + uses: nikitasavinov/checkstyle-action@d87d526a914fc5cb0b003908e35038dbb2d6e1b7 + with: + # Report level for reviewdog [info,warning,error] + level: info + # Reporter of reviewdog command [github-pr-check,github-pr-review] + reporter: github-pr-check + # Filtering for the reviewdog command [added,diff_context,file,nofilter]. + filter_mode: added + # Exit code for reviewdog when errors are found [true,false]. + fail_on_error: false + # Checkstyle config file + checkstyle_config: minestom_checks.xml + checkstyle_version: "8.37" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6d233db27..c6d8bda7c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,17 @@ jobs: java-version: 1.11 - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup gradle cache + uses: burrunan/gradle-cache-action@v1 + with: + save-generated-gradle-jars: false + save-local-build-cache: false + save-gradle-dependencies-cache: true + save-maven-dependencies-cache: true + # Ignore some of the paths when caching Maven Local repository + maven-local-ignore-paths: | + net/minestom/ - name: Build Minestom - run: ./gradlew build - - name: Run tests + run: ./gradlew classes testClasses + - name: Run Minestom tests run: ./gradlew test diff --git a/build.gradle b/build.gradle index f84f82d6d..f01a523cc 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ plugins { id 'maven-publish' id 'net.ltgt.apt' version '0.10' id 'org.jetbrains.kotlin.jvm' version '1.4.10' + id 'checkstyle' } group 'net.minestom.server' @@ -36,7 +37,7 @@ allprojects { maven { url 'https://jitpack.io' } maven { name 'sponge' - url 'http://repo.spongepowered.org/maven' + url 'https://repo.spongepowered.org/maven' } } javadoc { @@ -45,6 +46,11 @@ allprojects { addBooleanOption('html5', true) } } + + checkstyle { + toolVersion "8.37" + configFile file("${projectDir}/minestom_checks.xml") + } } sourceSets { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a8e4b9ef2..35c7a66ae 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,3 +4,4 @@ distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionSha256Sum=0f316a67b971b7b571dac7215dcf2591a30994b3450e0629925ffcfe2c68cc5c diff --git a/minestom_checks.xml b/minestom_checks.xml new file mode 100644 index 000000000..eb513ae72 --- /dev/null +++ b/minestom_checks.xml @@ -0,0 +1,303 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 8097d16e6..6d817a075 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -43,6 +43,7 @@ import net.minestom.server.storage.StorageManager; import net.minestom.server.timer.SchedulerManager; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.PacketUtils; +import net.minestom.server.utils.cache.TemporaryCache; import net.minestom.server.utils.thread.MinestomThread; import net.minestom.server.utils.validate.Check; import net.minestom.server.world.Difficulty; @@ -77,7 +78,7 @@ public final class MinecraftServer { public static final String THREAD_NAME_TICK = "Ms-Tick"; public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool"; - public static final int THREAD_COUNT_BLOCK_BATCH = 2; + public static final int THREAD_COUNT_BLOCK_BATCH = 4; public static final String THREAD_NAME_SCHEDULER = "Ms-SchedulerPool"; public static final int THREAD_COUNT_SCHEDULER = 1; @@ -125,7 +126,7 @@ public final class MinecraftServer { private static boolean initialized; private static boolean started; - private static int chunkViewDistance = 10; + private static int chunkViewDistance = 8; private static int entityViewDistance = 5; private static int compressionThreshold = 256; private static ResponseDataConsumer responseDataConsumer; @@ -674,6 +675,7 @@ public final class MinecraftServer { LOGGER.info("Shutting down all thread pools."); benchmarkManager.disable(); commandManager.stopConsoleThread(); + TemporaryCache.REMOVER_SERVICE.shutdown(); MinestomThread.shutdownAll(); LOGGER.info("Minestom server stopped successfully."); } diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index b4de068c8..49d705fc0 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -1,6 +1,5 @@ package net.minestom.server.entity; -import io.netty.channel.Channel; import net.minestom.server.MinecraftServer; import net.minestom.server.advancements.AdvancementTab; import net.minestom.server.attribute.Attribute; @@ -57,7 +56,6 @@ 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.instance.InstanceUtils; -import net.minestom.server.utils.player.PlayerUtils; import net.minestom.server.utils.time.CooldownUtils; import net.minestom.server.utils.time.TimeUnit; import net.minestom.server.utils.time.UpdateOption; @@ -83,7 +81,7 @@ public class Player extends LivingEntity implements CommandSender { /** * @see #getPlayerSynchronizationGroup() */ - private static volatile int playerSynchronizationGroup = 100; + private static volatile int playerSynchronizationGroup = 50; /** * For the number of viewers that a player has, the position synchronization packet will be sent @@ -341,15 +339,8 @@ public class Player extends LivingEntity implements CommandSender { @Override public void update(long time) { - - // Flush all pending packets - if (PlayerUtils.isNettyClient(this)) { - Channel channel = ((NettyPlayerConnection) playerConnection).getChannel(); - channel.eventLoop().execute(channel::flush); - } - - // Network tick verification - playerConnection.updateStats(); + // Network tick + this.playerConnection.update(); // Process received packets ClientPlayPacket packet; @@ -706,7 +697,7 @@ public class Player extends LivingEntity implements CommandSender { sendDimension(instanceDimensionType); } - final long[] visibleChunks = ChunkUtils.getChunksInRange(position, getChunkRange()); + final long[] visibleChunks = ChunkUtils.getChunksInRange(firstSpawn ? getRespawnPoint() : position, getChunkRange()); final int length = visibleChunks.length; AtomicInteger counter = new AtomicInteger(0); @@ -750,12 +741,17 @@ public class Player extends LivingEntity implements CommandSender { */ private void spawnPlayer(Instance instance, boolean firstSpawn) { this.viewableEntities.forEach(entity -> entity.removeViewer(this)); - super.setInstance(instance); if (firstSpawn) { - teleport(getRespawnPoint()); + this.position = getRespawnPoint(); + this.cacheX = position.getX(); + this.cacheY = position.getY(); + this.cacheZ = position.getZ(); + updatePlayerPosition(); } + super.setInstance(instance); + PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn); callEvent(PlayerSpawnEvent.class, spawnEvent); } @@ -1166,8 +1162,7 @@ public class Player extends LivingEntity implements CommandSender { /** * Gets the player display name in the tab-list. * - * @return the player display name, - * null means that {@link #getUsername()} is displayed + * @return the player display name, null means that {@link #getUsername()} is displayed */ @Nullable public ColoredText getDisplayName() { @@ -1377,11 +1372,11 @@ public class Player extends LivingEntity implements CommandSender { *

* Can be altered by the {@link PlayerRespawnEvent#setRespawnPosition(Position)}. * - * @return the default respawn point + * @return a copy of the default respawn point */ @NotNull public Position getRespawnPoint() { - return respawnPoint; + return respawnPoint.copy(); } /** diff --git a/src/main/java/net/minestom/server/entity/type/animal/EntityLlama.java b/src/main/java/net/minestom/server/entity/type/animal/EntityLlama.java new file mode 100644 index 000000000..9f3891d5d --- /dev/null +++ b/src/main/java/net/minestom/server/entity/type/animal/EntityLlama.java @@ -0,0 +1,13 @@ +package net.minestom.server.entity.type.animal; + +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.type.Animal; +import net.minestom.server.utils.Position; + +public class EntityLlama extends EntityCreature implements Animal { + public EntityLlama(Position spawnPosition) { + super(EntityType.LLAMA, spawnPosition); + setBoundingBox(0.45f, 0.9375f, 0.45f); + } +} diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index 7b5cad348..e2327e266 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -61,6 +61,8 @@ public abstract class Chunk implements Viewable, DataContainer { public static final int BIOME_COUNT = 1024; // 4x4x4 blocks group + private final UUID identifier; + @NotNull protected final Biome[] biomes; protected final int chunkX, chunkZ; @@ -79,6 +81,7 @@ public abstract class Chunk implements Viewable, DataContainer { protected Data data; public Chunk(@Nullable Biome[] biomes, int chunkX, int chunkZ, boolean shouldGenerate) { + this.identifier = UUID.randomUUID(); this.chunkX = chunkX; this.chunkZ = chunkZ; this.shouldGenerate = shouldGenerate; @@ -191,6 +194,17 @@ public abstract class Chunk implements Viewable, DataContainer { @NotNull public abstract Set getBlockEntities(); + /** + * Gets the last time that this chunk changed. + *

+ * "Change" means here data used in {@link ChunkDataPacket}. + * It is necessary to see if the cached version of this chunk can be used + * instead of re writing and compressing everything. + * + * @return the last change time in milliseconds + */ + public abstract long getLastChangeTime(); + /** * Serializes the chunk into bytes. * @@ -259,6 +273,18 @@ public abstract class Chunk implements Viewable, DataContainer { return getCustomBlock(x, y, z); } + /** + * Gets the unique identifier of this chunk. + *

+ * WARNING: this UUID is not persistent but randomized once the object is instantiate. + * + * @return the chunk identifier + */ + @NotNull + public UUID getIdentifier() { + return identifier; + } + public Biome[] getBiomes() { return biomes; } @@ -448,7 +474,7 @@ public abstract class Chunk implements Viewable, DataContainer { // TODO do not hardcode light { - UpdateLightPacket updateLightPacket = new UpdateLightPacket(); + UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime()); updateLightPacket.chunkX = getChunkX(); updateLightPacket.chunkZ = getChunkZ(); updateLightPacket.skyLightMask = 0x3FFF0; diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 4def0f0b1..524041707 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -57,6 +57,8 @@ public class DynamicChunk extends Chunk { // Block entities protected final Set blockEntities = new CopyOnWriteArraySet<>(); + private long lastChangeTime; + public DynamicChunk(@Nullable Biome[] biomes, int chunkX, int chunkZ, @NotNull PaletteStorage blockPalette, @NotNull PaletteStorage customBlockPalette) { super(biomes, chunkX, chunkZ, true); @@ -86,8 +88,8 @@ public class DynamicChunk extends Chunk { // True if the block is not complete air without any custom block capabilities final boolean hasBlock = blockStateId != 0 || customBlockId != 0; - this.blockPalette.setBlockAt(x, y, z, blockStateId); - this.customBlockPalette.setBlockAt(x, y, z, customBlockId); + setBlockAt(blockPalette, x, y, z, blockStateId); + setBlockAt(customBlockPalette, x, y, z, customBlockId); if (!hasBlock) { // Block has been deleted, clear cache and return @@ -155,23 +157,23 @@ public class DynamicChunk extends Chunk { @Override public short getBlockStateId(int x, int y, int z) { - return this.blockPalette.getBlockAt(x, y, z); + return getBlockAt(blockPalette, x, y, z); } @Override public short getCustomBlockId(int x, int y, int z) { - return customBlockPalette.getBlockAt(x, y, z); + return getBlockAt(customBlockPalette, x, y, z); } @Override protected void refreshBlockValue(int x, int y, int z, short blockStateId, short customBlockId) { - this.blockPalette.setBlockAt(x, y, z, blockStateId); - this.customBlockPalette.setBlockAt(x, y, z, customBlockId); + setBlockAt(blockPalette, x, y, z, blockStateId); + setBlockAt(customBlockPalette, x, y, z, customBlockId); } @Override protected void refreshBlockStateId(int x, int y, int z, short blockStateId) { - this.blockPalette.setBlockAt(x, y, z, blockStateId); + setBlockAt(blockPalette, x, y, z, blockStateId); } @Override @@ -195,6 +197,11 @@ public class DynamicChunk extends Chunk { return blockEntities; } + @Override + public long getLastChangeTime() { + return lastChangeTime; + } + /** * Serialize this {@link Chunk} based on {@link #readChunk(BinaryReader, ChunkCallback)} *

@@ -242,8 +249,8 @@ public class DynamicChunk extends Chunk { for (byte z = 0; z < CHUNK_SIZE_Z; z++) { final int index = getBlockIndex(x, y, z); - final short blockStateId = blockPalette.getBlockAt(x, y, z); - final short customBlockId = customBlockPalette.getBlockAt(x, y, z); + final short blockStateId = getBlockAt(blockPalette, x, y, z); + final short customBlockId = getBlockAt(customBlockPalette, x, y, z); // No block at the position if (blockStateId == 0 && customBlockId == 0) @@ -376,7 +383,7 @@ public class DynamicChunk extends Chunk { @NotNull @Override protected ChunkDataPacket createFreshPacket() { - ChunkDataPacket fullDataPacket = new ChunkDataPacket(); + ChunkDataPacket fullDataPacket = new ChunkDataPacket(getIdentifier(), getLastChangeTime()); fullDataPacket.biomes = biomes.clone(); fullDataPacket.chunkX = chunkX; fullDataPacket.chunkZ = chunkZ; @@ -400,4 +407,13 @@ public class DynamicChunk extends Chunk { return dynamicChunk; } + + private short getBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z) { + return paletteStorage.getBlockAt(x, y, z); + } + + private void setBlockAt(@NotNull PaletteStorage paletteStorage, int x, int y, int z, short blockId) { + paletteStorage.setBlockAt(x, y, z, blockId); + this.lastChangeTime = System.currentTimeMillis(); + } } \ No newline at end of file diff --git a/src/main/java/net/minestom/server/listener/manager/PacketConsumer.java b/src/main/java/net/minestom/server/listener/manager/ClientPacketConsumer.java similarity index 57% rename from src/main/java/net/minestom/server/listener/manager/PacketConsumer.java rename to src/main/java/net/minestom/server/listener/manager/ClientPacketConsumer.java index 689db0669..fc8110f39 100644 --- a/src/main/java/net/minestom/server/listener/manager/PacketConsumer.java +++ b/src/main/java/net/minestom/server/listener/manager/ClientPacketConsumer.java @@ -2,23 +2,21 @@ package net.minestom.server.listener.manager; import net.minestom.server.entity.Player; import net.minestom.server.network.ConnectionManager; +import net.minestom.server.network.packet.client.ClientPlayPacket; import org.jetbrains.annotations.NotNull; /** - * Interface used to add a listener for incoming/outgoing packets with - * {@link ConnectionManager#onPacketReceive(PacketConsumer)} and {@link ConnectionManager#onPacketSend(PacketConsumer)}. - * - * @param the packet type + * Interface used to add a listener for incoming packets with {@link ConnectionManager#onPacketReceive(ClientPacketConsumer)}. */ @FunctionalInterface -public interface PacketConsumer { +public interface ClientPacketConsumer { /** - * Called when a packet is received/sent from/to a client. + * Called when a packet is received from a client. * * @param player the player concerned by the packet * @param packetController the packet controller, can be used to cancel the packet * @param packet the packet */ - void accept(@NotNull Player player, @NotNull PacketController packetController, @NotNull T packet); + void accept(@NotNull Player player, @NotNull PacketController packetController, @NotNull ClientPlayPacket packet); } diff --git a/src/main/java/net/minestom/server/listener/manager/PacketController.java b/src/main/java/net/minestom/server/listener/manager/PacketController.java index 1716777f3..c628090c3 100644 --- a/src/main/java/net/minestom/server/listener/manager/PacketController.java +++ b/src/main/java/net/minestom/server/listener/manager/PacketController.java @@ -1,9 +1,14 @@ package net.minestom.server.listener.manager; import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.client.ClientPlayPacket; +import net.minestom.server.network.packet.server.ServerPacket; + +import java.util.Collection; /** - * Used to control the output of a packet in {@link PacketConsumer#accept(Player, PacketController, Object)}. + * Used to control the output of a packet in {@link ClientPacketConsumer#accept(Player, PacketController, ClientPlayPacket)} + * and {@link ServerPacketConsumer#accept(Collection, PacketController, ServerPacket)}. */ public class PacketController { diff --git a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java index f153afd0c..09594d762 100644 --- a/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java +++ b/src/main/java/net/minestom/server/listener/manager/PacketListenerManager.java @@ -11,6 +11,8 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -73,8 +75,8 @@ public final class PacketListenerManager { } final PacketController packetController = new PacketController(); - for (PacketConsumer packetConsumer : CONNECTION_MANAGER.getReceivePacketConsumers()) { - packetConsumer.accept(player, packetController, packet); + for (ClientPacketConsumer clientPacketConsumer : CONNECTION_MANAGER.getReceivePacketConsumers()) { + clientPacketConsumer.accept(player, packetController, packet); } if (packetController.isCancel()) @@ -85,17 +87,21 @@ public final class PacketListenerManager { } /** - * Executes the consumers from {@link ConnectionManager#onPacketSend(PacketConsumer)}. + * Executes the consumers from {@link ConnectionManager#onPacketSend(ServerPacketConsumer)}. * - * @param packet the packet to process - * @param player the player which should receive the packet - * @param the packet type + * @param packet the packet to process + * @param players the players which should receive the packet * @return true if the packet is not cancelled, false otherwise */ - public boolean processServerPacket(@NotNull T packet, @NotNull Player player) { + public boolean processServerPacket(@NotNull ServerPacket packet, @NotNull Collection players) { + final List consumers = CONNECTION_MANAGER.getSendPacketConsumers(); + if (consumers.isEmpty()) { + return true; + } + final PacketController packetController = new PacketController(); - for (PacketConsumer packetConsumer : CONNECTION_MANAGER.getSendPacketConsumers()) { - packetConsumer.accept(player, packetController, packet); + for (ServerPacketConsumer serverPacketConsumer : consumers) { + serverPacketConsumer.accept(players, packetController, packet); } return !packetController.isCancel(); diff --git a/src/main/java/net/minestom/server/listener/manager/ServerPacketConsumer.java b/src/main/java/net/minestom/server/listener/manager/ServerPacketConsumer.java new file mode 100644 index 000000000..24ceae99e --- /dev/null +++ b/src/main/java/net/minestom/server/listener/manager/ServerPacketConsumer.java @@ -0,0 +1,25 @@ +package net.minestom.server.listener.manager; + +import net.minestom.server.entity.Player; +import net.minestom.server.network.ConnectionManager; +import net.minestom.server.network.packet.server.ServerPacket; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Interface used to add a listener for outgoing packets with {@link ConnectionManager#onPacketSend(ServerPacketConsumer)}. + */ +@FunctionalInterface +public interface ServerPacketConsumer { + + /** + * Called when a packet is sent to a client. + * + * @param players the players who will receive the packet + * @param packetController the packet controller, can be used for cancelling + * @param packet the packet to send + */ + void accept(@NotNull Collection players, @NotNull PacketController packetController, @NotNull ServerPacket packet); + +} diff --git a/src/main/java/net/minestom/server/map/MapColors.java b/src/main/java/net/minestom/server/map/MapColors.java index f158d73fd..4871bde02 100644 --- a/src/main/java/net/minestom/server/map/MapColors.java +++ b/src/main/java/net/minestom/server/map/MapColors.java @@ -123,10 +123,10 @@ public enum MapColors { // From the wiki: https://minecraft.gamepedia.com/Map_item_format // Map Color ID Multiply R,G,B By = Multiplier - //Base Color ID×4 + 0 180 0.71 - //Base Color ID×4 + 1 220 0.86 - //Base Color ID×4 + 2 255 (same color) 1 - //Base Color ID×4 + 3 135 0.53 + //Base Color ID*4 + 0 180 0.71 + //Base Color ID*4 + 1 220 0.86 + //Base Color ID*4 + 2 255 (same color) 1 + //Base Color ID*4 + 3 135 0.53 /** * Returns the color index with RGB multiplied by 0.53, to use on a map diff --git a/src/main/java/net/minestom/server/network/ConnectionManager.java b/src/main/java/net/minestom/server/network/ConnectionManager.java index 4043048b9..027fcfe87 100644 --- a/src/main/java/net/minestom/server/network/ConnectionManager.java +++ b/src/main/java/net/minestom/server/network/ConnectionManager.java @@ -3,10 +3,9 @@ package net.minestom.server.network; import net.minestom.server.chat.JsonMessage; import net.minestom.server.entity.Player; import net.minestom.server.entity.fakeplayer.FakePlayer; -import net.minestom.server.listener.manager.PacketConsumer; -import net.minestom.server.network.packet.client.ClientPlayPacket; +import net.minestom.server.listener.manager.ClientPacketConsumer; +import net.minestom.server.listener.manager.ServerPacketConsumer; import net.minestom.server.network.packet.client.login.LoginStartPacket; -import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.login.LoginSuccessPacket; import net.minestom.server.network.packet.server.play.ChatMessagePacket; import net.minestom.server.network.player.PlayerConnection; @@ -29,9 +28,9 @@ public final class ConnectionManager { private final Map connectionPlayerMap = Collections.synchronizedMap(new HashMap<>()); // All the consumers to call once a packet is received - private final List> receivePacketConsumers = new CopyOnWriteArrayList<>(); + private final List receiveClientPacketConsumers = new CopyOnWriteArrayList<>(); // All the consumers to call once a packet is sent - private final List> sendPacketConsumers = new CopyOnWriteArrayList<>(); + private final List sendClientPacketConsumers = new CopyOnWriteArrayList<>(); // The uuid provider once a player login private UuidProvider uuidProvider; // The player provider to have your own Player implementation @@ -145,43 +144,39 @@ public final class ConnectionManager { /** * Gets all the listeners which are called for each packet received. * - * @return an unmodifiable list of packet's consumers + * @return a list of packet's consumers */ @NotNull - public List> getReceivePacketConsumers() { - return Collections.unmodifiableList(receivePacketConsumers); + public List getReceivePacketConsumers() { + return receiveClientPacketConsumers; } /** * Adds a consumer to call once a packet is received. * - * @param packetConsumer the packet consumer + * @param clientPacketConsumer the packet consumer */ - public void onPacketReceive(@NotNull PacketConsumer packetConsumer) { - this.receivePacketConsumers.add(packetConsumer); + public void onPacketReceive(@NotNull ClientPacketConsumer clientPacketConsumer) { + this.receiveClientPacketConsumers.add(clientPacketConsumer); } /** * Gets all the listeners which are called for each packet sent. * - * @return an unmodifiable list of packet's consumers + * @return a list of packet's consumers */ @NotNull - public List> getSendPacketConsumers() { - return Collections.unmodifiableList(sendPacketConsumers); + public List getSendPacketConsumers() { + return Collections.unmodifiableList(sendClientPacketConsumers); } /** * Adds a consumer to call once a packet is sent. - *

- * Be aware that it is possible for the same packet instance to be used multiple time, - * changing the object fields could lead to issues. - * (consider canceling the packet instead and send your own) * - * @param packetConsumer the packet consumer + * @param serverPacketConsumer the packet consumer */ - public void onPacketSend(@NotNull PacketConsumer packetConsumer) { - this.sendPacketConsumers.add(packetConsumer); + public void onPacketSend(@NotNull ServerPacketConsumer serverPacketConsumer) { + this.sendClientPacketConsumers.add(serverPacketConsumer); } /** diff --git a/src/main/java/net/minestom/server/network/netty/NettyServer.java b/src/main/java/net/minestom/server/network/netty/NettyServer.java index 35044129d..c3b94b75f 100644 --- a/src/main/java/net/minestom/server/network/netty/NettyServer.java +++ b/src/main/java/net/minestom/server/network/netty/NettyServer.java @@ -12,18 +12,40 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.traffic.ChannelTrafficShapingHandler; +import io.netty.handler.traffic.GlobalChannelTrafficShapingHandler; +import io.netty.handler.traffic.TrafficCounter; +import net.minestom.server.MinecraftServer; import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.netty.channel.ClientChannel; -import net.minestom.server.network.netty.codec.LegacyPingHandler; -import net.minestom.server.network.netty.codec.PacketDecoder; -import net.minestom.server.network.netty.codec.PacketEncoder; -import net.minestom.server.network.netty.codec.PacketFramer; +import net.minestom.server.network.netty.codec.*; import org.jetbrains.annotations.NotNull; import java.net.InetSocketAddress; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; -public class NettyServer { +public final class NettyServer { + + private static final long DEFAULT_COMPRESSED_CHANNEL_WRITE_LIMIT = 600_000L; + private static final long DEFAULT_COMPRESSED_CHANNEL_READ_LIMIT = 100_000L; + + private static final long DEFAULT_UNCOMPRESSED_CHANNEL_WRITE_LIMIT = 15_000_000L; + private static final long DEFAULT_UNCOMPRESSED_CHANNEL_READ_LIMIT = 1_000_000L; + + public static final String TRAFFIC_LIMITER_HANDLER_NAME = "traffic-limiter"; // Read/write + public static final String LEGACY_PING_HANDLER_NAME = "legacy-ping"; // Read + + public static final String ENCRYPT_HANDLER_NAME = "encrypt"; // Write + public static final String DECRYPT_HANDLER_NAME = "decrypt"; // Read + + public static final String GROUPED_PACKET_HANDLER_NAME = "grouped-packet"; // Write + public static final String FRAMER_HANDLER_NAME = "framer"; // Read/write + + public static final String COMPRESSOR_HANDLER_NAME = "compressor"; // Read/write + + public static final String DECODER_HANDLER_NAME = "decoder"; // Read + public static final String ENCODER_HANDLER_NAME = "encoder"; // Write + public static final String CLIENT_CHANNEL_NAME = "handler"; // Read private final EventLoopGroup boss, worker; private final ServerBootstrap bootstrap; @@ -33,9 +55,12 @@ public class NettyServer { private String address; private int port; - // Options - private long writeLimit = 750_000L; - private long readLimit = 750_000L; + private final GlobalChannelTrafficShapingHandler globalTrafficHandler; + + /** + * Scheduler used by {@code globalTrafficHandler}. + */ + private final ScheduledExecutorService trafficScheduler = Executors.newScheduledThreadPool(1); public NettyServer(@NotNull PacketProcessor packetProcessor) { Class channel; @@ -61,37 +86,65 @@ public class NettyServer { .group(boss, worker) .channel(channel); + this.globalTrafficHandler = new GlobalChannelTrafficShapingHandler(trafficScheduler, 200) { + @Override + protected void doAccounting(TrafficCounter counter) { + // TODO proper monitoring API + //System.out.println("data " + counter.lastWriteThroughput() / 1000 + " " + counter.lastReadThroughput() / 1000); + } + }; + + bootstrap.childHandler(new ChannelInitializer() { protected void initChannel(@NotNull SocketChannel ch) { ChannelConfig config = ch.config(); config.setOption(ChannelOption.TCP_NODELAY, true); + config.setOption(ChannelOption.SO_SNDBUF, 1_000_000); ChannelPipeline pipeline = ch.pipeline(); - ChannelTrafficShapingHandler channelTrafficShapingHandler = - new ChannelTrafficShapingHandler(writeLimit, readLimit, 200); - - pipeline.addLast("traffic-limiter", channelTrafficShapingHandler); + pipeline.addLast(TRAFFIC_LIMITER_HANDLER_NAME, globalTrafficHandler); // First check should verify if the packet is a legacy ping (from 1.6 version and earlier) // Removed from the pipeline later in LegacyPingHandler if unnecessary (>1.6) - pipeline.addLast("legacy-ping", new LegacyPingHandler()); + pipeline.addLast(LEGACY_PING_HANDLER_NAME, new LegacyPingHandler()); + + // Used to bypass all the previous handlers by directly sending a framed buffer + pipeline.addLast(GROUPED_PACKET_HANDLER_NAME, new GroupedPacketHandler()); // Adds packetLength at start | Reads framed bytebuf - pipeline.addLast("framer", new PacketFramer(packetProcessor)); + pipeline.addLast(FRAMER_HANDLER_NAME, new PacketFramer(packetProcessor)); // Reads bytebuf and creating inbound packet - pipeline.addLast("decoder", new PacketDecoder()); + pipeline.addLast(DECODER_HANDLER_NAME, new PacketDecoder()); // Writes packet to bytebuf - pipeline.addLast("encoder", new PacketEncoder()); + pipeline.addLast(ENCODER_HANDLER_NAME, new PacketEncoder()); - pipeline.addLast("handler", new ClientChannel(packetProcessor)); + pipeline.addLast(CLIENT_CHANNEL_NAME, new ClientChannel(packetProcessor)); } }); } - public void start(String address, int port) { + /** + * Binds the address to start the server. + * + * @param address the server address + * @param port the server port + */ + public void start(@NotNull String address, int port) { + + { + final boolean compression = MinecraftServer.getCompressionThreshold() != 0; + if (compression) { + globalTrafficHandler.setWriteChannelLimit(DEFAULT_COMPRESSED_CHANNEL_WRITE_LIMIT); + globalTrafficHandler.setReadChannelLimit(DEFAULT_COMPRESSED_CHANNEL_READ_LIMIT); + } else { + globalTrafficHandler.setWriteChannelLimit(DEFAULT_UNCOMPRESSED_CHANNEL_WRITE_LIMIT); + globalTrafficHandler.setReadChannelLimit(DEFAULT_UNCOMPRESSED_CHANNEL_READ_LIMIT); + } + } + this.address = address; this.port = port; @@ -127,58 +180,27 @@ public class NettyServer { } /** - * Gets the server write limit. + * Gets the traffic handler, used to control channel and global bandwidth. *

- * Used when you want to limit the bandwidth used by a single connection. - * Can also prevent the networking threads from being unresponsive. + * The object can be modified as specified by Netty documentation. * - * @return the write limit in bytes + * @return the global traffic handler */ - public long getWriteLimit() { - return writeLimit; + @NotNull + public GlobalChannelTrafficShapingHandler getGlobalTrafficHandler() { + return globalTrafficHandler; } /** - * Changes the server write limit - *

- * WARNING: the change will only apply to new connections, the current ones will not be updated. - * - * @param writeLimit the new write limit in bytes, 0 to disable - * @see #getWriteLimit() + * Stops the server and the various services. */ - public void setWriteLimit(long writeLimit) { - this.writeLimit = writeLimit; - } - - - /** - * Gets the server read limit. - *

- * Used when you want to limit the bandwidth used by a single connection. - * Can also prevent the networking threads from being unresponsive. - * - * @return the read limit in bytes - */ - public long getReadLimit() { - return readLimit; - } - - /** - * Changes the server read limit - *

- * WARNING: the change will only apply to new connections, the current ones will not be updated. - * - * @param readLimit the new read limit in bytes, 0 to disable - * @see #getWriteLimit() - */ - public void setReadLimit(long readLimit) { - this.readLimit = readLimit; - } - public void stop() { - serverChannel.close(); + this.serverChannel.close(); - worker.shutdownGracefully(); - boss.shutdownGracefully(); + this.worker.shutdownGracefully(); + this.boss.shutdownGracefully(); + + this.trafficScheduler.shutdown(); + this.globalTrafficHandler.release(); } } diff --git a/src/main/java/net/minestom/server/network/netty/codec/GroupedPacketHandler.java b/src/main/java/net/minestom/server/network/netty/codec/GroupedPacketHandler.java new file mode 100644 index 000000000..ba23a268b --- /dev/null +++ b/src/main/java/net/minestom/server/network/netty/codec/GroupedPacketHandler.java @@ -0,0 +1,14 @@ +package net.minestom.server.network.netty.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import net.minestom.server.network.netty.packet.FramedPacket; + +public class GroupedPacketHandler extends MessageToByteEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, FramedPacket msg, ByteBuf out) { + out.writeBytes(msg.body.retainedSlice()); + } +} diff --git a/src/main/java/net/minestom/server/network/netty/codec/PacketCompressor.java b/src/main/java/net/minestom/server/network/netty/codec/PacketCompressor.java index 4e9c8ed8c..79f09af67 100644 --- a/src/main/java/net/minestom/server/network/netty/codec/PacketCompressor.java +++ b/src/main/java/net/minestom/server/network/netty/codec/PacketCompressor.java @@ -21,6 +21,7 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageCodec; import io.netty.handler.codec.DecoderException; +import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.Utils; import java.util.List; @@ -33,7 +34,7 @@ public class PacketCompressor extends ByteToMessageCodec { private final byte[] buffer = new byte[8192]; - private final Deflater deflater = new Deflater(); + private final Deflater deflater = new Deflater(3); private final Inflater inflater = new Inflater(); public PacketCompressor(int threshold) { @@ -42,24 +43,7 @@ public class PacketCompressor extends ByteToMessageCodec { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) { - final int packetLength = from.readableBytes(); - - if (packetLength < this.threshold) { - Utils.writeVarIntBuf(to, 0); - to.writeBytes(from); - } else { - Utils.writeVarIntBuf(to, packetLength); - - deflater.setInput(from.nioBuffer()); - deflater.finish(); - - while (!deflater.finished()) { - final int length = deflater.deflate(buffer); - to.writeBytes(buffer, 0, length); - } - - deflater.reset(); - } + PacketUtils.compressBuffer(deflater, buffer, from, to); } @Override diff --git a/src/main/java/net/minestom/server/network/netty/codec/PacketFramer.java b/src/main/java/net/minestom/server/network/netty/codec/PacketFramer.java index 62af408d0..a342a73e4 100644 --- a/src/main/java/net/minestom/server/network/netty/codec/PacketFramer.java +++ b/src/main/java/net/minestom/server/network/netty/codec/PacketFramer.java @@ -7,6 +7,7 @@ import io.netty.handler.codec.CorruptedFrameException; import net.minestom.server.MinecraftServer; import net.minestom.server.network.PacketProcessor; import net.minestom.server.network.player.PlayerConnection; +import net.minestom.server.utils.PacketUtils; import net.minestom.server.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,17 +26,7 @@ public class PacketFramer extends ByteToMessageCodec { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) { - final int packetSize = from.readableBytes(); - final int headerSize = Utils.getVarIntSize(packetSize); - - if (headerSize > 3) { - throw new IllegalStateException("Unable to fit " + headerSize + " into 3"); - } - - to.ensureWritable(packetSize + headerSize); - - Utils.writeVarIntBuf(to, packetSize); - to.writeBytes(from, from.readerIndex(), packetSize); + PacketUtils.frameBuffer(from, to); } @Override diff --git a/src/main/java/net/minestom/server/network/netty/packet/FramedPacket.java b/src/main/java/net/minestom/server/network/netty/packet/FramedPacket.java new file mode 100644 index 000000000..4466539b7 --- /dev/null +++ b/src/main/java/net/minestom/server/network/netty/packet/FramedPacket.java @@ -0,0 +1,18 @@ +package net.minestom.server.network.netty.packet; + +import io.netty.buffer.ByteBuf; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a packet which is already framed. + * Can be used if you want to send the exact same buffer to multiple clients without processing it more than once. + */ +public class FramedPacket { + + public final ByteBuf body; + + public FramedPacket(@NotNull ByteBuf body) { + this.body = body; + } + +} diff --git a/src/main/java/net/minestom/server/network/packet/client/login/LoginStartPacket.java b/src/main/java/net/minestom/server/network/packet/client/login/LoginStartPacket.java index 3741eeed7..437f26417 100644 --- a/src/main/java/net/minestom/server/network/packet/client/login/LoginStartPacket.java +++ b/src/main/java/net/minestom/server/network/packet/client/login/LoginStartPacket.java @@ -39,7 +39,7 @@ public class LoginStartPacket implements ClientPreplayPacket { // Compression final int threshold = MinecraftServer.getCompressionThreshold(); if (threshold > 0) { - nettyPlayerConnection.enableCompression(threshold); + nettyPlayerConnection.startCompression(); } } 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 1be950606..1bb6e5f6b 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 @@ -13,16 +13,21 @@ import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.BufUtils; import net.minestom.server.utils.Utils; import net.minestom.server.utils.binary.BinaryWriter; +import net.minestom.server.utils.cache.CacheablePacket; +import net.minestom.server.utils.cache.TemporaryPacketCache; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.world.biomes.Biome; 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 { +public class ChunkDataPacket implements ServerPacket, CacheablePacket { private static final BlockManager BLOCK_MANAGER = MinecraftServer.getBlockManager(); + private static final TemporaryPacketCache CACHE = new TemporaryPacketCache(10000L); public boolean fullChunk; public Biome[] biomes; @@ -40,6 +45,15 @@ public class ChunkDataPacket implements ServerPacket { private static final int MAX_BITS_PER_ENTRY = 16; private static final int MAX_BUFFER_SIZE = (Short.BYTES + Byte.BYTES + 5 * Byte.BYTES + (4096 * MAX_BITS_PER_ENTRY / Long.SIZE * Long.BYTES)) * CHUNK_SECTION_COUNT + 256 * Integer.BYTES; + // Cacheable data + private UUID identifier; + private long lastUpdate; + + public ChunkDataPacket(@Nullable UUID identifier, long lastUpdate) { + this.identifier = identifier; + this.lastUpdate = lastUpdate; + } + @Override public void write(@NotNull BinaryWriter writer) { writer.writeInt(chunkX); @@ -122,4 +136,19 @@ public class ChunkDataPacket implements ServerPacket { public int getId() { return ServerPacketIdentifier.CHUNK_DATA; } + + @Override + public TemporaryPacketCache getCache() { + return CACHE; + } + + @Override + public UUID getIdentifier() { + return identifier; + } + + @Override + public long getLastUpdateTime() { + return lastUpdate; + } } \ No newline at end of file diff --git a/src/main/java/net/minestom/server/network/packet/server/play/UpdateLightPacket.java b/src/main/java/net/minestom/server/network/packet/server/play/UpdateLightPacket.java index 6647d99ae..c2df52b61 100644 --- a/src/main/java/net/minestom/server/network/packet/server/play/UpdateLightPacket.java +++ b/src/main/java/net/minestom/server/network/packet/server/play/UpdateLightPacket.java @@ -3,11 +3,17 @@ package net.minestom.server.network.packet.server.play; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; import net.minestom.server.utils.binary.BinaryWriter; +import net.minestom.server.utils.cache.CacheablePacket; +import net.minestom.server.utils.cache.TemporaryPacketCache; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.UUID; -public class UpdateLightPacket implements ServerPacket { +public class UpdateLightPacket implements ServerPacket, CacheablePacket { + + private static final TemporaryPacketCache CACHE = new TemporaryPacketCache(10000L); public int chunkX; public int chunkZ; @@ -23,6 +29,15 @@ public class UpdateLightPacket implements ServerPacket { public List skyLight; public List blockLight; + // Cacheable data + private UUID identifier; + private long lastUpdate; + + public UpdateLightPacket(@Nullable UUID identifier, long lastUpdate) { + this.identifier = identifier; + this.lastUpdate = lastUpdate; + } + @Override public void write(@NotNull BinaryWriter writer) { writer.writeVarInt(chunkX); @@ -53,4 +68,19 @@ public class UpdateLightPacket implements ServerPacket { public int getId() { return ServerPacketIdentifier.UPDATE_LIGHT; } + + @Override + public TemporaryPacketCache getCache() { + return CACHE; + } + + @Override + public UUID getIdentifier() { + return identifier; + } + + @Override + public long getLastUpdateTime() { + return lastUpdate; + } } 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 caa75fc54..1db7b9b2c 100644 --- a/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java @@ -1,5 +1,6 @@ package net.minestom.server.network.player; +import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.socket.SocketChannel; import net.minestom.server.MinecraftServer; @@ -8,9 +9,14 @@ import net.minestom.server.extras.mojangAuth.Decrypter; import net.minestom.server.extras.mojangAuth.Encrypter; import net.minestom.server.extras.mojangAuth.MojangCrypt; import net.minestom.server.network.ConnectionState; +import net.minestom.server.network.netty.NettyServer; import net.minestom.server.network.netty.codec.PacketCompressor; +import net.minestom.server.network.netty.packet.FramedPacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.login.SetCompressionPacket; +import net.minestom.server.utils.PacketUtils; +import net.minestom.server.utils.cache.CacheablePacket; +import net.minestom.server.utils.cache.TemporaryCache; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -57,6 +63,14 @@ public class NettyPlayerConnection extends PlayerConnection { this.remoteAddress = channel.remoteAddress(); } + @Override + public void update() { + // Flush + this.channel.flush(); + // Network stats + super.update(); + } + /** * Sets the encryption key and add the codecs to the pipeline. * @@ -66,21 +80,26 @@ public class NettyPlayerConnection extends PlayerConnection { public void setEncryptionKey(@NotNull SecretKey secretKey) { Check.stateCondition(encrypted, "Encryption is already enabled!"); this.encrypted = true; - channel.pipeline().addBefore("framer", "decrypt", new Decrypter(MojangCrypt.getCipher(2, secretKey))); - channel.pipeline().addBefore("framer", "encrypt", new Encrypter(MojangCrypt.getCipher(1, secretKey))); + channel.pipeline().addBefore(NettyServer.FRAMER_HANDLER_NAME, NettyServer.DECRYPT_HANDLER_NAME, + new Decrypter(MojangCrypt.getCipher(2, secretKey))); + channel.pipeline().addBefore(NettyServer.FRAMER_HANDLER_NAME, NettyServer.ENCRYPT_HANDLER_NAME, + new Encrypter(MojangCrypt.getCipher(1, secretKey))); } /** * Enables compression and add a new codec to the pipeline. * - * @param threshold the threshold for a packet to be compressible * @throws IllegalStateException if encryption is already enabled for this connection */ - public void enableCompression(int threshold) { + public void startCompression() { Check.stateCondition(compressed, "Compression is already enabled!"); + final int threshold = MinecraftServer.getCompressionThreshold(); + Check.stateCondition(threshold == 0, "Compression cannot be enabled because the threshold is equal to 0"); + this.compressed = true; sendPacket(new SetCompressionPacket(threshold)); - channel.pipeline().addAfter("framer", "compressor", new PacketCompressor(threshold)); + channel.pipeline().addAfter(NettyServer.FRAMER_HANDLER_NAME, NettyServer.COMPRESSOR_HANDLER_NAME, + new PacketCompressor(threshold)); } /** @@ -93,26 +112,63 @@ public class NettyPlayerConnection extends PlayerConnection { @Override public void sendPacket(@NotNull ServerPacket serverPacket) { if (shouldSendPacket(serverPacket)) { - if (getPlayer() != null) { // Flush on player update - if (MinecraftServer.processingNettyErrors()) - channel.write(serverPacket).addListener(future -> { - if (!future.isSuccess()) { - future.cause().printStackTrace(); + if (getPlayer() != null) { + // Flush happen during #update() + if (serverPacket instanceof CacheablePacket) { + CacheablePacket cacheablePacket = (CacheablePacket) serverPacket; + final UUID identifier = cacheablePacket.getIdentifier(); + + if (identifier == null) { + // This packet explicitly said to do not retrieve the cache + if (MinecraftServer.processingNettyErrors()) + channel.write(serverPacket).addListener(future -> { + if (!future.isSuccess()) { + future.cause().printStackTrace(); + } + }); + else + channel.write(serverPacket, channel.voidPromise()); + } else { + // Try to retrieve the cached buffer + TemporaryCache temporaryCache = cacheablePacket.getCache(); + ByteBuf buffer = temporaryCache.retrieve(identifier); + if (buffer == null) { + // Buffer not found, create and cache it + final long time = System.currentTimeMillis(); + buffer = PacketUtils.createFramedPacket(serverPacket); + temporaryCache.cacheObject(identifier, buffer, time); } - }); - else { - channel.write(serverPacket, channel.voidPromise()); + + FramedPacket framedPacket = new FramedPacket(buffer); + if (MinecraftServer.processingNettyErrors()) + channel.write(framedPacket).addListener(future -> { + if (!future.isSuccess()) { + future.cause().printStackTrace(); + } + }); + else + channel.write(framedPacket, channel.voidPromise()); + } + + } else { + if (MinecraftServer.processingNettyErrors()) + channel.write(serverPacket).addListener(future -> { + if (!future.isSuccess()) { + future.cause().printStackTrace(); + } + }); + else + channel.write(serverPacket, channel.voidPromise()); } } else { - if (MinecraftServer.processingNettyErrors()) - channel.writeAndFlush(serverPacket).addListener(future -> { - if (!future.isSuccess()) { - future.cause().printStackTrace(); - } - }); - else { - channel.writeAndFlush(serverPacket, channel.voidPromise()); - } + if (MinecraftServer.processingNettyErrors()) + channel.writeAndFlush(serverPacket).addListener(future -> { + if (!future.isSuccess()) { + future.cause().printStackTrace(); + } + }); + else + channel.writeAndFlush(serverPacket, channel.voidPromise()); } } } @@ -136,7 +192,7 @@ public class NettyPlayerConnection extends PlayerConnection { @Override public void disconnect() { - channel.close(); + this.channel.close(); } @NotNull diff --git a/src/main/java/net/minestom/server/network/player/PlayerConnection.java b/src/main/java/net/minestom/server/network/player/PlayerConnection.java index 6f8c2a705..f6d921811 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerConnection.java @@ -4,8 +4,8 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.chat.ChatColor; import net.minestom.server.chat.ColoredText; import net.minestom.server.entity.Player; -import net.minestom.server.listener.manager.PacketConsumer; import net.minestom.server.listener.manager.PacketListenerManager; +import net.minestom.server.listener.manager.ServerPacketConsumer; import net.minestom.server.network.ConnectionManager; import net.minestom.server.network.ConnectionState; import net.minestom.server.network.packet.server.ServerPacket; @@ -15,6 +15,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.net.SocketAddress; +import java.util.Collections; import java.util.concurrent.atomic.AtomicInteger; /** @@ -45,7 +46,7 @@ public abstract class PlayerConnection { /** * Updates values related to the network connection. */ - public void updateStats() { + public void update() { // Check rate limit if (MinecraftServer.getRateLimit() > 0) { tickCounter++; @@ -92,7 +93,7 @@ public abstract class PlayerConnection { /** * Serializes the packet and send it to the client. *

- * Also responsible for executing {@link ConnectionManager#onPacketSend(PacketConsumer)} consumers. + * Also responsible for executing {@link ConnectionManager#onPacketSend(ServerPacketConsumer)} consumers. * * @param serverPacket the packet to send * @see #shouldSendPacket(ServerPacket) @@ -100,7 +101,8 @@ public abstract class PlayerConnection { public abstract void sendPacket(@NotNull ServerPacket serverPacket); protected boolean shouldSendPacket(@NotNull ServerPacket serverPacket) { - return player == null || PACKET_LISTENER_MANAGER.processServerPacket(serverPacket, player); + return player == null || + PACKET_LISTENER_MANAGER.processServerPacket(serverPacket, Collections.singleton(player)); } /** diff --git a/src/main/java/net/minestom/server/utils/PacketUtils.java b/src/main/java/net/minestom/server/utils/PacketUtils.java index fc75d70e4..f7419e536 100644 --- a/src/main/java/net/minestom/server/utils/PacketUtils.java +++ b/src/main/java/net/minestom/server/utils/PacketUtils.java @@ -2,13 +2,19 @@ package net.minestom.server.utils; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; +import net.minestom.server.listener.manager.PacketListenerManager; +import net.minestom.server.network.netty.packet.FramedPacket; import net.minestom.server.network.packet.server.ServerPacket; import net.minestom.server.network.packet.server.ServerPacketIdentifier; +import net.minestom.server.network.player.NettyPlayerConnection; +import net.minestom.server.network.player.PlayerConnection; import net.minestom.server.utils.binary.BinaryWriter; import org.jetbrains.annotations.NotNull; import java.util.Collection; +import java.util.zip.Deflater; /** * Utils class for packets. Including writing a {@link ServerPacket} into a {@link ByteBuf} @@ -16,21 +22,41 @@ import java.util.Collection; */ public final class PacketUtils { + private static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager(); + + private static Deflater deflater = new Deflater(3); + private static byte[] buffer = new byte[8192]; + private PacketUtils() { } /** - * Sends a {@link ServerPacket} to multiple players. Mostly used for convenience. + * Sends a {@link ServerPacket} to multiple players. *

- * Be aware that this will cause the send packet listeners to be given the exact same packet object. + * Can drastically improve performance since the packet will not have to be processed as much. * * @param players the players to send the packet to * @param packet the packet to send to the players */ public static void sendGroupedPacket(@NotNull Collection players, @NotNull ServerPacket packet) { - for (Player player : players) { - player.getPlayerConnection().sendPacket(packet); + if (players.isEmpty()) + return; + + final boolean success = PACKET_LISTENER_MANAGER.processServerPacket(packet, players); + if (success) { + final ByteBuf finalBuffer = createFramedPacket(packet); + final FramedPacket framedPacket = new FramedPacket(finalBuffer); + + for (Player player : players) { + final PlayerConnection playerConnection = player.getPlayerConnection(); + if (playerConnection instanceof NettyPlayerConnection) { + final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection; + nettyPlayerConnection.getChannel().write(framedPacket); + } else { + playerConnection.sendPacket(packet); + } + } } } @@ -99,4 +125,92 @@ public final class PacketUtils { return writer.getBuffer(); } + /** + * Frames a buffer for it to be understood by a Minecraft client. + *

+ * The content of {@code packetBuffer} can be either a compressed or uncompressed packet buffer, + * it depends of it the client did receive a {@link net.minestom.server.network.packet.server.login.SetCompressionPacket} packet before. + * + * @param packetBuffer the buffer containing compressed or uncompressed packet data + * @param frameTarget the buffer which will receive the framed version of {@code from} + */ + public static void frameBuffer(@NotNull ByteBuf packetBuffer, @NotNull ByteBuf frameTarget) { + final int packetSize = packetBuffer.readableBytes(); + final int headerSize = Utils.getVarIntSize(packetSize); + + if (headerSize > 3) { + throw new IllegalStateException("Unable to fit " + headerSize + " into 3"); + } + + frameTarget.ensureWritable(packetSize + headerSize); + + Utils.writeVarIntBuf(frameTarget, packetSize); + frameTarget.writeBytes(packetBuffer, packetBuffer.readerIndex(), packetSize); + } + + /** + * Compress using zlib the content of a packet. + *

+ * {@code packetBuffer} needs to be the packet content without any header (if you want to use it to write a Minecraft packet). + * + * @param deflater the deflater for zlib compression + * @param buffer a cached buffer which will be used to store temporary the deflater output + * @param packetBuffer the buffer containing all the packet fields + * @param compressionTarget the buffer which will receive the compressed version of {@code packetBuffer} + */ + public static void compressBuffer(@NotNull Deflater deflater, @NotNull byte[] buffer, @NotNull ByteBuf packetBuffer, @NotNull ByteBuf compressionTarget) { + final int packetLength = packetBuffer.readableBytes(); + + if (packetLength < MinecraftServer.getCompressionThreshold()) { + Utils.writeVarIntBuf(compressionTarget, 0); + compressionTarget.writeBytes(packetBuffer); + } else { + Utils.writeVarIntBuf(compressionTarget, packetLength); + + deflater.setInput(packetBuffer.nioBuffer()); + deflater.finish(); + + while (!deflater.finished()) { + final int length = deflater.deflate(buffer); + compressionTarget.writeBytes(buffer, 0, length); + } + + deflater.reset(); + } + } + + /** + * Creates a "framed packet" (packet which can be send and understood by a Minecraft client) + * from a server packet. + *

+ * Can be used if you want to store a raw buffer and send it later without the additional writing cost. + * Compression is applied if {@link MinecraftServer#getCompressionThreshold()} is greater than 0. + * + * @param serverPacket the server packet to write + * @return the framed packet from the server one + */ + public static ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket) { + ByteBuf packetBuf = writePacket(serverPacket); + + // TODO use pooled buffers instead of unpooled ones + if (MinecraftServer.getCompressionThreshold() > 0) { + + ByteBuf compressedBuf = Unpooled.buffer(); + ByteBuf framedBuf = Unpooled.buffer(); + synchronized (deflater) { + compressBuffer(deflater, buffer, packetBuf, compressedBuf); + } + + frameBuffer(compressedBuf, framedBuf); + + return framedBuf; + } else { + ByteBuf framedBuf = Unpooled.buffer(); + frameBuffer(packetBuf, framedBuf); + + return framedBuf; + } + + } + } diff --git a/src/main/java/net/minestom/server/utils/cache/CacheablePacket.java b/src/main/java/net/minestom/server/utils/cache/CacheablePacket.java new file mode 100644 index 000000000..9c32f6103 --- /dev/null +++ b/src/main/java/net/minestom/server/utils/cache/CacheablePacket.java @@ -0,0 +1,45 @@ +package net.minestom.server.utils.cache; + +import net.minestom.server.network.packet.server.ServerPacket; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * Implemented by {@link ServerPacket server packets} which can be temporary cached in memory to be re-sent later + * without having to go through all the writing and compression. + *

+ * {@link #getIdentifier()} is to differenciate this packet from the others of the same type, + * and {@link #getLastUpdateTime()} to know if one packet is newer than the previous one. + */ +public interface CacheablePacket { + + /** + * Gets the cache linked to this packet. + *

+ * WARNING: the cache needs to be shared between all the object instances, tips is to make it static. + * + * @return the temporary packet cache + */ + @NotNull + TemporaryPacketCache getCache(); + + /** + * Gets the identifier of this packet. + *

+ * Used to verify if this packet is already cached or not. + * + * @return this packet identifier, null to do not retrieve the cache + */ + @Nullable + UUID getIdentifier(); + + /** + * Gets the last time this packet changed. + * + * @return the last packet update time in milliseconds + */ + long getLastUpdateTime(); + +} diff --git a/src/main/java/net/minestom/server/utils/cache/TemporaryCache.java b/src/main/java/net/minestom/server/utils/cache/TemporaryCache.java new file mode 100644 index 000000000..07ea8a51f --- /dev/null +++ b/src/main/java/net/minestom/server/utils/cache/TemporaryCache.java @@ -0,0 +1,75 @@ +package net.minestom.server.utils.cache; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Cache objects with a timeout. + * + * @param the object type to cache + */ +public class TemporaryCache { + + public static final ScheduledExecutorService REMOVER_SERVICE = Executors.newScheduledThreadPool(1); + + // Identifier = Cached object + protected ConcurrentHashMap cache = new ConcurrentHashMap<>(); + // Identifier = time + protected ConcurrentHashMap cacheTime = new ConcurrentHashMap<>(); + + private long keepTime; + + /** + * Creates a new temporary cache. + * + * @param keepTime the time before considering an object unused in milliseconds + * @see #getKeepTime() + */ + public TemporaryCache(long keepTime) { + this.keepTime = keepTime; + REMOVER_SERVICE.scheduleAtFixedRate(() -> { + final boolean removed = cacheTime.values().removeIf(time -> System.currentTimeMillis() > time + keepTime); + if (removed) { + this.cache.entrySet().removeIf(entry -> !cacheTime.containsKey(entry.getKey())); + } + }, keepTime, keepTime, TimeUnit.MILLISECONDS); + } + + /** + * Caches an object + * + * @param identifier the object identifier + * @param value the object to cache + * @param time the current time in milliseconds + */ + public synchronized void cacheObject(@NotNull UUID identifier, T value, long time) { + this.cache.put(identifier, value); + this.cacheTime.put(identifier, time); + } + + /** + * Retrieves an object from cache. + * + * @param identifier the object identifier + * @return the retrieved object or null if not found + */ + @Nullable + public T retrieve(@NotNull UUID identifier) { + return cache.get(identifier); + } + + /** + * Gets the time an object will be kept without being retrieved + * + * @return the keep time in milliseconds + */ + public long getKeepTime() { + return keepTime; + } +} diff --git a/src/main/java/net/minestom/server/utils/cache/TemporaryPacketCache.java b/src/main/java/net/minestom/server/utils/cache/TemporaryPacketCache.java new file mode 100644 index 000000000..4e7b975b0 --- /dev/null +++ b/src/main/java/net/minestom/server/utils/cache/TemporaryPacketCache.java @@ -0,0 +1,12 @@ +package net.minestom.server.utils.cache; + +import io.netty.buffer.ByteBuf; + +/** + * Convenient superclass of {@link TemporaryCache} explicitly for packet to store a {@link ByteBuf}. + */ +public class TemporaryPacketCache extends TemporaryCache { + public TemporaryPacketCache(long keepTime) { + super(keepTime); + } +} diff --git a/src/main/java/net/minestom/server/utils/debug/DebugUtils.java b/src/main/java/net/minestom/server/utils/debug/DebugUtils.java index bdd926e6f..44c43edc9 100644 --- a/src/main/java/net/minestom/server/utils/debug/DebugUtils.java +++ b/src/main/java/net/minestom/server/utils/debug/DebugUtils.java @@ -11,15 +11,11 @@ public final class DebugUtils { public final static Logger LOGGER = LoggerFactory.getLogger(DebugUtils.class); private static final String LINE_SEPARATOR = System.getProperty("line.separator"); - private static final int OFFSET = 2; // Used to do not show DebugUtils in the stack trace /** * Prints the current thread stack trace elements. - * - * @param maxLine the maximum number of stack trace element */ - public static synchronized void printStackTrace(int maxLine) { - maxLine += OFFSET; + public static synchronized void printStackTrace() { StackTraceElement[] elements = Thread.currentThread().getStackTrace(); StringBuilder stringBuilder = new StringBuilder(); @@ -27,7 +23,7 @@ public final class DebugUtils { stringBuilder.append("START STACKTRACE"); stringBuilder.append(LINE_SEPARATOR); - for (int i = OFFSET; i < maxLine; i++) { + for (int i = 0; i < Integer.MAX_VALUE; i++) { if (i >= elements.length) break; @@ -42,11 +38,4 @@ public final class DebugUtils { LOGGER.info(stringBuilder.toString()); } - /** - * Prints the current thread stack trace elements. - */ - public static synchronized void printStackTrace() { - printStackTrace(Integer.MAX_VALUE); - } - } diff --git a/src/test/java/demo/MainDemo.java b/src/test/java/demo/MainDemo.java index f71101ece..6c0fcd6d6 100644 --- a/src/test/java/demo/MainDemo.java +++ b/src/test/java/demo/MainDemo.java @@ -6,7 +6,6 @@ import net.minestom.server.instance.*; import net.minestom.server.instance.batch.ChunkBatch; import net.minestom.server.instance.block.Block; import net.minestom.server.network.ConnectionManager; -import net.minestom.server.network.netty.NettyServer; import net.minestom.server.utils.Position; import net.minestom.server.world.biomes.Biome; @@ -37,14 +36,6 @@ public class MainDemo { }); }); - // OPTIONAL: optimize networking to prevent having unresponsive threads - { - NettyServer nettyServer = MinecraftServer.getNettyServer(); - // Set the maximum bandwidth out and in to 500KB/s, largely enough for a single client - nettyServer.setWriteLimit(500_000); - nettyServer.setReadLimit(500_000); - } - // Start the server minecraftServer.start("localhost", 25565); } diff --git a/src/test/java/demo/commands/TestCommand.java b/src/test/java/demo/commands/TestCommand.java index 87bb21c39..e58c80e82 100644 --- a/src/test/java/demo/commands/TestCommand.java +++ b/src/test/java/demo/commands/TestCommand.java @@ -5,6 +5,7 @@ import net.minestom.server.command.builder.Arguments; import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.entity.Player; public class TestCommand extends Command { @@ -19,11 +20,12 @@ public class TestCommand extends Command { }); setDefaultExecutor((source, args) -> { - System.out.println("DEFAULT"); System.gc(); + source.sendMessage("Explicit GC executed!"); }); addSyntax((source, args) -> { + Player player = (Player) source; System.out.println("ARG 1"); }, test); }