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 extends ServerChannel> 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);
}