Conflicts:
	src/main/java/net/minestom/server/entity/Player.java
	src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java
	src/main/java/net/minestom/server/utils/PacketUtils.java
This commit is contained in:
Eoghanmc22 2020-11-20 08:56:58 -05:00
commit 9a64a0a409
33 changed files with 1033 additions and 227 deletions

31
.github/workflows/check-pr-style.yml vendored Normal file
View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -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

303
minestom_checks.xml Normal file
View File

@ -0,0 +1,303 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<module name="SuppressWarningsFilter" />
<module name="SuppressionSingleFilter">
<property name="files" value="autogenerated"/>
<property name="checks" value=".*"/>
</module>
<!-- Excludes all 'module-info.java' files -->
<!-- See https://checkstyle.org/config_filefilters.html -->
<module name="BeforeExecutionExclusionFileFilter">
<property name="fileNamePattern" value="module\-info\.java$"/>
</module>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<!-- Checks whether files end with a new line. -->
<!-- See https://checkstyle.org/config_misc.html#NewlineAtEndOfFile -->
<module name="NewlineAtEndOfFile"/>
<!-- Miscellaneous other checks. -->
<!-- See https://checkstyle.org/config_misc.html -->
<module name="RegexpSingleline">
<property name="format" value="\s+$"/>
<property name="minimum" value="0"/>
<property name="maximum" value="0"/>
<property name="message" value="Line has trailing spaces."/>
</module>
<module name="LineLength">
<property name="fileExtensions" value="java"/>
<property name="max" value="120"/>
<property name="ignorePattern" value="^package.*|^import.*|^ *\* *.+$|a href|href|http://|https://|ftp://"/>
</module>
<module name="TreeWalker">
<module name="SuppressWarningsHolder" />
<module name="SuppressionCommentFilter">
<property name="onCommentFormat" value="\@formatter\:on" />
<property name="offCommentFormat" value="\@formatter\:off" />
</module>
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<!-- Checks for imports -->
<!-- See https://checkstyle.org/config_import.html -->
<module name="AvoidStarImport"/>
<module name="IllegalImport"/> <!-- defaults to sun.* packages -->
<module name="RedundantImport"/>
<module name="UnusedImports">
<property name="processJavadoc" value="true"/>
</module>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
</module>
<!-- Modifier Checks -->
<!-- See https://checkstyle.org/config_modifiers.html -->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<module name="FinalClass"/>
<module name="TodoComment">
<property name="severity" value="info" />
<property name="format" value="(TODO)|(FIXME)"/>
</module>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly"/>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyLambdas" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="4"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="4"/>
<property name="throwsIndent" value="8"/>
<property name="lineWrappingIndentation" value="8"/>
<property name="arrayInitIndent" value="4"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="MethodParamPad"/>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad"/>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="InvalidJavadocPosition"/>
<module name="JavadocTagContinuationIndentation" />
<module name="SummaryJavadoc">
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph">
<property name="allowNewlineParagraph" value="false" />
</module>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="scope" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="allowedAnnotations" value="Override, Test"/>
</module>
<module name="MissingJavadocMethod">
<property name="scope" value="public"/>
<property name="minLineCount" value="2"/>
<property name="allowedAnnotations" value="Override, Test"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="ignoreInlineTags" value="false"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation"/>
</module>
</module>

View File

@ -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.");
}

View File

@ -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 {
* <p>
* 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();
}
/**

View File

@ -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);
}
}

View File

@ -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<Integer> getBlockEntities();
/**
* Gets the last time that this chunk changed.
* <p>
* "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.
* <p>
* 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;

View File

@ -57,6 +57,8 @@ public class DynamicChunk extends Chunk {
// Block entities
protected final Set<Integer> 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)}
* <p>
@ -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();
}
}

View File

@ -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 <T> the packet type
* Interface used to add a listener for incoming packets with {@link ConnectionManager#onPacketReceive(ClientPacketConsumer)}.
*/
@FunctionalInterface
public interface PacketConsumer<T> {
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);
}

View File

@ -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 {

View File

@ -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<ClientPlayPacket> 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 <T> 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 <T extends ServerPacket> boolean processServerPacket(@NotNull T packet, @NotNull Player player) {
public boolean processServerPacket(@NotNull ServerPacket packet, @NotNull Collection<Player> players) {
final List<ServerPacketConsumer> consumers = CONNECTION_MANAGER.getSendPacketConsumers();
if (consumers.isEmpty()) {
return true;
}
final PacketController packetController = new PacketController();
for (PacketConsumer<ServerPacket> packetConsumer : CONNECTION_MANAGER.getSendPacketConsumers()) {
packetConsumer.accept(player, packetController, packet);
for (ServerPacketConsumer serverPacketConsumer : consumers) {
serverPacketConsumer.accept(players, packetController, packet);
}
return !packetController.isCancel();

View File

@ -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<Player> players, @NotNull PacketController packetController, @NotNull ServerPacket packet);
}

View File

@ -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

View File

@ -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<PlayerConnection, Player> connectionPlayerMap = Collections.synchronizedMap(new HashMap<>());
// All the consumers to call once a packet is received
private final List<PacketConsumer<ClientPlayPacket>> receivePacketConsumers = new CopyOnWriteArrayList<>();
private final List<ClientPacketConsumer> receiveClientPacketConsumers = new CopyOnWriteArrayList<>();
// All the consumers to call once a packet is sent
private final List<PacketConsumer<ServerPacket>> sendPacketConsumers = new CopyOnWriteArrayList<>();
private final List<ServerPacketConsumer> 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<PacketConsumer<ClientPlayPacket>> getReceivePacketConsumers() {
return Collections.unmodifiableList(receivePacketConsumers);
public List<ClientPacketConsumer> 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<ClientPlayPacket> 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<PacketConsumer<ServerPacket>> getSendPacketConsumers() {
return Collections.unmodifiableList(sendPacketConsumers);
public List<ServerPacketConsumer> getSendPacketConsumers() {
return Collections.unmodifiableList(sendClientPacketConsumers);
}
/**
* Adds a consumer to call once a packet is sent.
* <p>
* 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<ServerPacket> packetConsumer) {
this.sendPacketConsumers.add(packetConsumer);
public void onPacketSend(@NotNull ServerPacketConsumer serverPacketConsumer) {
this.sendClientPacketConsumers.add(serverPacketConsumer);
}
/**

View File

@ -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<SocketChannel>() {
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.
* <p>
* 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
* <p>
* 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.
* <p>
* 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
* <p>
* 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();
}
}

View File

@ -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<FramedPacket> {
@Override
protected void encode(ChannelHandlerContext ctx, FramedPacket msg, ByteBuf out) {
out.writeBytes(msg.body.retainedSlice());
}
}

View File

@ -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<ByteBuf> {
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<ByteBuf> {
@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

View File

@ -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<ByteBuf> {
@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

View File

@ -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;
}
}

View File

@ -39,7 +39,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
// Compression
final int threshold = MinecraftServer.getCompressionThreshold();
if (threshold > 0) {
nettyPlayerConnection.enableCompression(threshold);
nettyPlayerConnection.startCompression();
}
}

View File

@ -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;
}
}

View File

@ -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<byte[]> skyLight;
public List<byte[]> 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;
}
}

View File

@ -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<ByteBuf> 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

View File

@ -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.
* <p>
* 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));
}
/**

View File

@ -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.
* <p>
* 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<Player> 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.
* <p>
* 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.
* <p>
* {@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.
* <p>
* 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;
}
}
}

View File

@ -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.
* <p>
* {@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.
* <p>
* 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.
* <p>
* 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();
}

View File

@ -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 <T> the object type to cache
*/
public class TemporaryCache<T> {
public static final ScheduledExecutorService REMOVER_SERVICE = Executors.newScheduledThreadPool(1);
// Identifier = Cached object
protected ConcurrentHashMap<UUID, T> cache = new ConcurrentHashMap<>();
// Identifier = time
protected ConcurrentHashMap<UUID, Long> 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;
}
}

View File

@ -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<ByteBuf> {
public TemporaryPacketCache(long keepTime) {
super(keepTime);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}