Merge pull request #389 from Minestom/nio

Switch to NIO
This commit is contained in:
TheMode 2021-08-09 00:46:56 +02:00 committed by GitHub
commit 1181724b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1052 additions and 1984 deletions

View File

@ -107,12 +107,6 @@ dependencies {
// Only here to ensure J9 module support for extensions and our classloaders
testCompileOnly 'org.mockito:mockito-core:3.11.1'
// Netty
api 'io.netty:netty-handler:4.1.65.Final'
api 'io.netty:netty-codec:4.1.65.Final'
api 'io.netty:netty-transport-native-epoll:4.1.65.Final:linux-x86_64'
api 'io.netty:netty-transport-native-kqueue:4.1.65.Final:osx-x86_64'
// https://mvnrepository.com/artifact/it.unimi.dsi/fastutil
api 'it.unimi.dsi:fastutil:8.5.4'

View File

@ -21,10 +21,10 @@ import net.minestom.server.listener.manager.PacketListenerManager;
import net.minestom.server.monitoring.BenchmarkManager;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.NettyServer;
import net.minestom.server.network.packet.server.play.PluginMessagePacket;
import net.minestom.server.network.packet.server.play.ServerDifficultyPacket;
import net.minestom.server.network.packet.server.play.UpdateViewDistancePacket;
import net.minestom.server.network.socket.Server;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.scoreboard.TeamManager;
@ -44,6 +44,9 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
/**
* The main server class used to start the server and retrieve all the managers.
* <p>
@ -85,9 +88,7 @@ public final class MinecraftServer {
// Network
private static PacketListenerManager packetListenerManager;
private static PacketProcessor packetProcessor;
private static NettyServer nettyServer;
private static int nettyThreadCount = Runtime.getRuntime().availableProcessors();
private static boolean processNettyErrors = true;
private static Server server;
private static ExceptionManager exceptionManager;
@ -122,7 +123,6 @@ public final class MinecraftServer {
private static int chunkViewDistance = 8;
private static int entityViewDistance = 5;
private static int compressionThreshold = 256;
private static boolean packetCaching = true;
private static boolean groupedPacket = true;
private static boolean terminalEnabled = System.getProperty("minestom.terminal.disabled") == null;
private static ResponseDataConsumer responseDataConsumer;
@ -168,7 +168,11 @@ public final class MinecraftServer {
tagManager = new TagManager();
nettyServer = new NettyServer(packetProcessor);
try {
server = new Server(packetProcessor);
} catch (IOException e) {
e.printStackTrace();
}
initialized = true;
@ -277,16 +281,6 @@ public final class MinecraftServer {
return packetListenerManager;
}
/**
* Gets the netty server.
*
* @return the netty server
*/
public static NettyServer getNettyServer() {
checkInitStatus(nettyServer);
return nettyServer;
}
/**
* Gets the manager handling all registered instances.
*
@ -519,34 +513,6 @@ public final class MinecraftServer {
MinecraftServer.compressionThreshold = compressionThreshold;
}
/**
* Gets if the packet caching feature is enabled.
* <p>
* This feature allows some packets (implementing the {@link net.minestom.server.utils.cache.CacheablePacket} to be cached
* in order to do not have to be written and compressed over and over again), this is especially useful for chunk and light packets.
* <p>
* It is enabled by default and it is our recommendation,
* you should only disable it if you want to focus on low memory usage
* at the cost of many packet writing and compression.
*
* @return true if the packet caching feature is enabled, false otherwise
*/
public static boolean hasPacketCaching() {
return packetCaching;
}
/**
* Enables or disable packet caching.
*
* @param packetCaching true to enable packet caching
* @throws IllegalStateException if this is called after the server started
* @see #hasPacketCaching()
*/
public static void setPacketCaching(boolean packetCaching) {
Check.stateCondition(started, "You cannot change the packet caching value after the server has been started.");
MinecraftServer.packetCaching = packetCaching;
}
/**
* Gets if the packet caching feature is enabled.
* <p>
@ -666,45 +632,9 @@ public final class MinecraftServer {
return updateManager;
}
/**
* Gets the number of threads used by Netty.
* <p>
* Is the number of vCPU by default.
*
* @return the number of netty threads
*/
public static int getNettyThreadCount() {
return nettyThreadCount;
}
/**
* Changes the number of threads used by Netty.
*
* @param nettyThreadCount the number of threads
* @throws IllegalStateException if the server is already started
*/
public static void setNettyThreadCount(int nettyThreadCount) {
Check.stateCondition(started, "Netty thread count can only be changed before the server starts!");
MinecraftServer.nettyThreadCount = nettyThreadCount;
}
/**
* Gets if the server should process netty errors and other unnecessary netty events.
*
* @return should process netty errors
*/
public static boolean shouldProcessNettyErrors() {
return processNettyErrors;
}
/**
* Sets if the server should process netty errors and other unnecessary netty events.
* false is faster
*
* @param processNettyErrors should process netty errors
*/
public static void setShouldProcessNettyErrors(boolean processNettyErrors) {
MinecraftServer.processNettyErrors = processNettyErrors;
public static Server getServer() {
checkInitStatus(server);
return server;
}
/**
@ -744,8 +674,11 @@ public final class MinecraftServer {
updateManager.start();
// Init & start the TCP server
nettyServer.init();
nettyServer.start(address, port);
try {
server.start(new InetSocketAddress(address, port));
} catch (IOException e) {
e.printStackTrace();
}
if (extensionManager.shouldLoadOnStartup()) {
final long loadStartTime = System.nanoTime();
@ -779,7 +712,7 @@ public final class MinecraftServer {
updateManager.stop();
schedulerManager.shutdown();
connectionManager.shutdown();
nettyServer.stop();
server.stop();
storageManager.getLoadedLocations().forEach(StorageLocation::close);
LOGGER.info("Unloading all extensions.");
extensionManager.shutdown();

View File

@ -1,15 +1,15 @@
package net.minestom.server;
import net.minestom.server.acquirable.Acquirable;
import net.minestom.server.entity.Player;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.monitoring.TickMonitor;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.thread.SingleThreadProvider;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.async.AsyncUtils;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -39,13 +39,13 @@ public final class UpdateManager {
/**
* Should only be created in MinecraftServer.
*/
protected UpdateManager() {
UpdateManager() {
}
/**
* Starts the server loop in the update thread.
*/
protected void start() {
void start() {
final ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
new Thread(() -> {
@ -82,10 +82,12 @@ public final class UpdateManager {
}
// Flush all waiting packets
AsyncUtils.runAsync(() -> connectionManager.getOnlinePlayers().parallelStream()
.filter(player -> player.getPlayerConnection() instanceof NettyPlayerConnection)
.map(player -> (NettyPlayerConnection) player.getPlayerConnection())
.forEach(NettyPlayerConnection::flush));
for (Player player : connectionManager.getOnlinePlayers()) {
final var connection = player.getPlayerConnection();
if (connection instanceof PlayerSocketConnection) {
((PlayerSocketConnection) connection).flush();
}
}
// Disable thread until next tick
LockSupport.parkNanos((long) ((MinecraftServer.TICK_MS * 1e6) - tickTime));
@ -93,6 +95,7 @@ public final class UpdateManager {
MinecraftServer.getExceptionManager().handleException(e);
}
}
this.threadProvider.shutdown();
}, MinecraftServer.THREAD_NAME_TICK_SCHEDULER).start();
}
@ -239,6 +242,5 @@ public final class UpdateManager {
*/
public void stop() {
this.stopRequested = true;
this.threadProvider.shutdown();
}
}

View File

@ -1,6 +1,5 @@
package net.minestom.server.command.builder.arguments;
import io.netty.util.internal.StringUtil;
import net.minestom.server.command.builder.NodeMaker;
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
import net.minestom.server.network.packet.server.play.DeclareCommandsPacket;
@ -48,11 +47,10 @@ public class ArgumentString extends Argument<String> {
*/
@Deprecated
public static String staticParse(@NotNull String input) throws ArgumentSyntaxException {
// Return if not quoted
if (!input.contains(String.valueOf(DOUBLE_QUOTE)) &&
!input.contains(String.valueOf(QUOTE)) &&
!input.contains(String.valueOf(StringUtil.SPACE))) {
!input.contains(StringUtils.SPACE)) {
return input;
}

View File

@ -33,6 +33,7 @@ import net.minestom.server.potion.TimedPotion;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.thread.ThreadProvider;
import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.entity.EntityUtils;
import net.minestom.server.utils.player.PlayerUtils;
@ -490,14 +491,14 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
}
private void velocityTick() {
final boolean isNettyClient = PlayerUtils.isNettyClient(this);
final boolean isSocketClient = PlayerUtils.isSocketClient(this);
final boolean noGravity = hasNoGravity();
final boolean hasVelocity = hasVelocity();
boolean applyVelocity;
// Non-player entities with either velocity or gravity enabled
applyVelocity = !isNettyClient && (hasVelocity || !noGravity);
applyVelocity = !isSocketClient && (hasVelocity || !noGravity);
// Players with a velocity applied (client is responsible for gravity)
applyVelocity |= isNettyClient && hasVelocity;
applyVelocity |= isSocketClient && hasVelocity;
if (!applyVelocity) {
return;
}
@ -536,13 +537,13 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
return;
}
refreshPosition(finalVelocityPosition, true);
if (!isNettyClient) {
if (!isSocketClient) {
synchronizePosition(true);
}
// Update velocity
if (hasVelocity || !newVelocity.isZero()) {
if (onGround && isNettyClient) {
if (onGround && isSocketClient) {
// Stop player velocity
this.velocity = Vec.ZERO;
} else {
@ -708,6 +709,7 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
public CompletableFuture<Void> setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) {
Check.stateCondition(!instance.isRegistered(),
"Instances need to be registered, please use InstanceManager#registerInstance or InstanceManager#registerSharedInstance");
if (isRemoved()) return AsyncUtils.VOID_FUTURE;
if (this.instance != null) {
this.instance.UNSAFE_removeEntity(this);
}
@ -1267,16 +1269,15 @@ public class Entity implements Viewable, Tickable, TagHandler, PermissionHandler
* WARNING: this does not trigger {@link EntityDeathEvent}.
*/
public void remove() {
if (isRemoved())
return;
if (isRemoved()) return;
MinecraftServer.getUpdateManager().getThreadProvider().removeEntity(this);
this.removed = true;
this.shouldRemove = true;
Entity.ENTITY_BY_ID.remove(id);
Entity.ENTITY_BY_UUID.remove(uuid);
if (instance != null)
if (instance != null) {
instance.UNSAFE_removeEntity(this);
}
}
/**

View File

@ -55,8 +55,8 @@ import net.minestom.server.network.packet.client.play.ClientChatMessagePacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.packet.server.play.*;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.recipe.Recipe;
import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.resourcepack.ResourcePack;
@ -92,7 +92,7 @@ import java.util.function.UnaryOperator;
/**
* Those are the major actors of the server,
* they are not necessary backed by a {@link NettyPlayerConnection} as shown by {@link FakePlayer}.
* they are not necessary backed by a {@link PlayerSocketConnection} as shown by {@link FakePlayer}.
* <p>
* You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}.
*/
@ -212,7 +212,7 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
* Init the player and spawn him.
* <p>
* WARNING: executed in the main update thread
* UNSAFE: Only meant to be used when a netty player connects through the server.
* UNSAFE: Only meant to be used when a socket player connects through the server.
*
* @param spawnInstance the player spawn instance (defined in {@link PlayerLoginEvent})
*/
@ -441,18 +441,14 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
@Override
public void remove() {
if (isRemoved())
return;
if (isRemoved()) return;
EventDispatcher.call(new PlayerDisconnectEvent(this));
super.remove();
this.packets.clear();
if (getOpenInventory() != null)
if (getOpenInventory() != null) {
getOpenInventory().removeViewer(this);
}
MinecraftServer.getBossBarManager().removeAllBossBars(this);
// Advancement tabs cache
{
Set<AdvancementTab> advancementTabs = AdvancementTab.getTabs(this);
@ -462,15 +458,10 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
}
}
}
// Clear all viewable entities
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
// Clear all viewable chunks
this.viewableChunks.forEach(chunk -> {
if (chunk.isLoaded())
chunk.removeViewer(this);
});
playerConnection.disconnect();
this.viewableChunks.forEach(chunk -> chunk.removeViewer(this));
}
@Override
@ -1376,8 +1367,8 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
} else {
disconnectPacket = new DisconnectPacket(component);
}
if (playerConnection instanceof NettyPlayerConnection) {
((NettyPlayerConnection) playerConnection).writeAndFlush(disconnectPacket);
if (playerConnection instanceof PlayerSocketConnection) {
((PlayerSocketConnection) playerConnection).writeAndFlush(disconnectPacket);
playerConnection.disconnect();
} else {
playerConnection.sendPacket(disconnectPacket);

View File

@ -22,7 +22,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* A fake player will behave exactly the same way as would do a {@link Player} backed by a netty connection
* A fake player will behave exactly the same way as would do a {@link Player} backed by a socket connection
* (events, velocity, gravity, player list, etc...) with the exception that you need to control it server-side
* using a {@link FakePlayerController} (see {@link #getController()}).
* <p>

View File

@ -8,10 +8,8 @@ import org.jetbrains.annotations.Nullable;
import java.security.KeyPair;
public final class MojangAuth {
private static volatile boolean enabled = false;
private static KeyPair keyPair;
private static volatile KeyPair keyPair;
/**
* Enables mojang authentication on the server.
@ -21,19 +19,16 @@ public final class MojangAuth {
public static void init() {
Check.stateCondition(enabled, "Mojang auth is already enabled!");
Check.stateCondition(MinecraftServer.isStarted(), "The server has already been started!");
enabled = true;
MojangAuth.enabled = true;
// Generate necessary fields...
keyPair = MojangCrypt.generateKeyPair();
MojangAuth.keyPair = MojangCrypt.generateKeyPair();
}
public static boolean isEnabled() {
return enabled;
}
@Nullable
public static KeyPair getKeyPair() {
public static @Nullable KeyPair getKeyPair() {
return keyPair;
}
}

View File

@ -121,7 +121,7 @@ public class OpenToLAN {
* Performs the ping.
*/
private static void ping() {
if (MinecraftServer.getNettyServer().getPort() != 0) {
if (MinecraftServer.getServer().getPort() != 0) {
if (packet == null || eventCooldown.isReady(System.currentTimeMillis())) {
final ServerListPingEvent event = new ServerListPingEvent(OPEN_TO_LAN);
EventDispatcher.call(event);

View File

@ -1,54 +0,0 @@
package net.minestom.server.extras.mojangAuth;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
import javax.crypto.Cipher;
import javax.crypto.ShortBufferException;
public class CipherBase {
private final Cipher cipher;
private byte[] inTempArray = new byte[0];
private byte[] outTempArray = new byte[0];
protected CipherBase(@NotNull Cipher cipher) {
this.cipher = cipher;
}
private byte[] bufToByte(ByteBuf buffer) {
int remainingBytes = buffer.readableBytes();
// Need to resize temp array
if (inTempArray.length < remainingBytes) {
inTempArray = new byte[remainingBytes];
}
buffer.readBytes(inTempArray, 0, remainingBytes);
return inTempArray;
}
protected ByteBuf decrypt(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn) throws ShortBufferException {
int remainingBytes = byteBufIn.readableBytes();
byte[] bytes = bufToByte(byteBufIn);
ByteBuf outputBuffer = channelHandlerContext.alloc().heapBuffer(cipher.getOutputSize(remainingBytes));
outputBuffer.writerIndex(cipher.update(bytes, 0, remainingBytes, outputBuffer.array(), outputBuffer.arrayOffset()));
return outputBuffer;
}
protected void encrypt(ByteBuf byteBufIn, ByteBuf byteBufOut) throws ShortBufferException {
int remainingBytes = byteBufIn.readableBytes();
byte[] bytes = bufToByte(byteBufIn);
int newSize = cipher.getOutputSize(remainingBytes);
// Need to resize temp array
if (outTempArray.length < newSize) {
outTempArray = new byte[newSize];
}
byteBufOut.writeBytes(outTempArray, 0, cipher.update(bytes, 0, remainingBytes, outTempArray));
}
}

View File

@ -1,20 +0,0 @@
package net.minestom.server.extras.mojangAuth;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import javax.crypto.Cipher;
import java.util.List;
public class Decrypter extends MessageToMessageDecoder<ByteBuf> {
private final CipherBase cipher;
public Decrypter(Cipher cipher) {
this.cipher = new CipherBase(cipher);
}
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
list.add(this.cipher.decrypt(channelHandlerContext, byteBuf));
}
}

View File

@ -1,19 +0,0 @@
package net.minestom.server.extras.mojangAuth;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import javax.crypto.Cipher;
public class Encrypter extends MessageToByteEncoder<ByteBuf> {
private final CipherBase cipher;
public Encrypter(Cipher cipher) {
this.cipher = new CipherBase(cipher);
}
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBufIn, ByteBuf byteBufOut) throws Exception {
this.cipher.encrypt(byteBufIn, byteBufOut);
}
}

View File

@ -14,8 +14,7 @@ import java.security.*;
public final class MojangCrypt {
private static final Logger LOGGER = LogManager.getLogger();
@Nullable
public static KeyPair generateKeyPair() {
public static @Nullable KeyPair generateKeyPair() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(1024);
@ -27,8 +26,7 @@ public final class MojangCrypt {
}
}
@Nullable
public static byte[] digestData(String data, PublicKey publicKey, SecretKey secretKey) {
public static byte @Nullable [] digestData(String data, PublicKey publicKey, SecretKey secretKey) {
try {
return digestData("SHA-1", data.getBytes("ISO_8859_1"), secretKey.getEncoded(), publicKey.getEncoded());
} catch (UnsupportedEncodingException e) {
@ -37,15 +35,12 @@ public final class MojangCrypt {
}
}
@Nullable
private static byte[] digestData(String algorithm, byte[]... data) {
private static byte @Nullable [] digestData(String algorithm, byte[]... data) {
try {
MessageDigest digest = MessageDigest.getInstance(algorithm);
for (byte[] bytes : data) {
digest.update(bytes);
}
return digest.digest();
} catch (NoSuchAlgorithmException e) {
MinecraftServer.getExceptionManager().handleException(e);
@ -67,7 +62,6 @@ public final class MojangCrypt {
} catch (IllegalBlockSizeException | BadPaddingException var4) {
MinecraftServer.getExceptionManager().handleException(var4);
}
LOGGER.error("Cipher data failed!");
return null;
}
@ -80,7 +74,6 @@ public final class MojangCrypt {
} catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException var4) {
MinecraftServer.getExceptionManager().handleException(var4);
}
LOGGER.error("Cipher creation failed!");
return null;
}

View File

@ -1,7 +1,5 @@
package net.minestom.server.extras.query;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@ -23,6 +21,7 @@ import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Random;
@ -151,18 +150,18 @@ public class Query {
}
// get the contents
ByteBuf data = Unpooled.wrappedBuffer(packet.getData());
ByteBuffer data = ByteBuffer.wrap(packet.getData());
// check the magic field
if (data.readUnsignedShort() != 0xFEFD) {
if ((data.getShort() & 0xFFFF) != 0xFEFD) {
continue;
}
// now check the query type
byte type = data.readByte();
byte type = data.get();
if (type == 9) { // handshake
int sessionID = data.readInt();
int sessionID = data.getInt();
int challengeToken = RANDOM.nextInt();
CHALLENGE_TOKENS.put(challengeToken, packet.getSocketAddress());
@ -184,12 +183,12 @@ public class Query {
}
}
} else if (type == 0) { // stat
int sessionID = data.readInt();
int challengeToken = data.readInt();
int sessionID = data.getInt();
int challengeToken = data.getInt();
SocketAddress sender = packet.getSocketAddress();
if (CHALLENGE_TOKENS.containsKey(challengeToken) && CHALLENGE_TOKENS.get(challengeToken).equals(sender)) {
int remaining = data.readableBytes();
int remaining = data.remaining();
if (remaining == 0) { // basic
BasicQueryEvent event = new BasicQueryEvent(sender, sessionID);

View File

@ -142,7 +142,7 @@ public class BasicQueryResponse implements Writeable {
writer.writeNullTerminatedString(this.map, Query.CHARSET);
writer.writeNullTerminatedString(this.numPlayers, Query.CHARSET);
writer.writeNullTerminatedString(this.maxPlayers, Query.CHARSET);
writer.getBuffer().writeShortLE(MinecraftServer.getNettyServer().getPort());
writer.writeNullTerminatedString(Objects.requireNonNullElse(MinecraftServer.getNettyServer().getAddress(), ""), Query.CHARSET);
writer.getBuffer().putShort((short) MinecraftServer.getServer().getPort()); // TODO little endian?
writer.writeNullTerminatedString(Objects.requireNonNullElse(MinecraftServer.getServer().getAddress(), ""), Query.CHARSET);
}
}

View File

@ -20,8 +20,8 @@ public enum QueryKey {
MAP(() -> "world"),
NUM_PLAYERS("numplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size())),
MAX_PLAYERS("maxplayers", () -> String.valueOf(MinecraftServer.getConnectionManager().getOnlinePlayers().size() + 1)),
HOST_PORT("hostport", () -> String.valueOf(MinecraftServer.getNettyServer().getPort())),
HOST_IP("hostip", () -> Objects.requireNonNullElse(MinecraftServer.getNettyServer().getAddress(), "localhost"));
HOST_PORT("hostport", () -> String.valueOf(MinecraftServer.getServer().getPort())),
HOST_IP("hostip", () -> Objects.requireNonNullElse(MinecraftServer.getServer().getAddress(), "localhost"));
static QueryKey[] VALUES = QueryKey.values();

View File

@ -1,6 +1,5 @@
package net.minestom.server.extras.velocity;
import io.netty.buffer.ByteBuf;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.PlayerSkin;
import net.minestom.server.utils.binary.BinaryReader;
@ -10,6 +9,7 @@ import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -54,9 +54,11 @@ public final class VelocityProxy {
final byte[] signature = reader.readBytes(32);
ByteBuf buf = reader.getBuffer();
final byte[] data = new byte[buf.readableBytes()];
buf.getBytes(buf.readerIndex(), data);
ByteBuffer buf = reader.getBuffer();
buf.mark();
final byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.reset();
try {
final Mac mac = Mac.getInstance("HmacSHA256");

View File

@ -13,18 +13,18 @@ import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockGetter;
import net.minestom.server.instance.block.BlockSetter;
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.network.packet.server.play.UpdateLightPacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.chunk.ChunkSupplier;
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.*;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
// TODO light data & API
@ -126,11 +126,13 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
public abstract long getLastChangeTime();
/**
* Creates a {@link ChunkDataPacket} with this chunk data ready to be written.
* Sends the chunk data to {@code player}.
*
* @return a new chunk data packet
* @param player the player
*/
public abstract @NotNull ChunkDataPacket createChunkPacket();
public abstract void sendChunk(@NotNull Player player);
public abstract void sendChunk();
/**
* Creates a copy of this chunk, including blocks state id, custom block id, biomes, update data.
@ -152,7 +154,7 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
/**
* Gets the unique identifier of this chunk.
* <p>
* WARNING: this UUID is not persistent but randomized once the object is instantiate.
* WARNING: this UUID is not persistent but randomized once the object is instantiated.
*
* @return the chunk identifier
*/
@ -244,50 +246,6 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
this.columnarSpace = columnarSpace;
}
/**
* Gets the light packet of this chunk.
*
* @return the light packet
*/
@NotNull
public UpdateLightPacket getLightPacket() {
long skyMask = 0;
long blockMask = 0;
List<byte[]> skyLights = new ArrayList<>();
List<byte[]> blockLights = new ArrayList<>();
UpdateLightPacket updateLightPacket = new UpdateLightPacket(getIdentifier(), getLastChangeTime());
updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ();
updateLightPacket.skyLight = skyLights;
updateLightPacket.blockLight = blockLights;
final var sections = getSections();
for (var entry : sections.entrySet()) {
final int index = entry.getKey() + 1;
final Section section = entry.getValue();
final var skyLight = section.getSkyLight();
final var blockLight = section.getBlockLight();
if (!ArrayUtils.empty(skyLight)) {
skyLights.add(skyLight);
skyMask |= 1L << index;
}
if (!ArrayUtils.empty(blockLight)) {
blockLights.add(blockLight);
blockMask |= 1L << index;
}
}
updateLightPacket.skyLightMask = new long[]{skyMask};
updateLightPacket.blockLightMask = new long[]{blockMask};
updateLightPacket.emptySkyLightMask = new long[0];
updateLightPacket.emptyBlockLightMask = new long[0];
return updateLightPacket;
}
/**
* Used to verify if the chunk should still be kept in memory.
*
@ -365,34 +323,10 @@ public abstract class Chunk implements BlockGetter, BlockSetter, Viewable, Ticka
tag.write(nbt, value);
}
/**
* Sends the chunk data to {@code player}.
*
* @param player the player
*/
public synchronized void sendChunk(@NotNull Player player) {
// Only send loaded chunk
if (!isLoaded())
return;
final PlayerConnection playerConnection = player.getPlayerConnection();
playerConnection.sendPacket(getLightPacket());
playerConnection.sendPacket(createChunkPacket());
}
public synchronized void sendChunk() {
if (!isLoaded()) {
return;
}
sendPacketToViewers(getLightPacket());
sendPacketToViewers(createChunkPacket());
}
/**
* Sets the chunk as "unloaded".
*/
protected void unload() {
this.loaded = false;
ChunkDataPacket.CACHE.invalidate(getIdentifier());
UpdateLightPacket.CACHE.invalidate(getIdentifier());
}
}

View File

@ -4,16 +4,24 @@ import com.extollit.gaming.ai.path.model.ColumnarOcclusionFieldList;
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.pathfinding.PFBlock;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.network.packet.server.play.UpdateLightPacket;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.PacketUtils;
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 java.lang.ref.SoftReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -30,9 +38,10 @@ public class DynamicChunk extends Chunk {
protected final Int2ObjectOpenHashMap<Block> entries = new Int2ObjectOpenHashMap<>();
protected final Int2ObjectOpenHashMap<Block> tickableMap = new Int2ObjectOpenHashMap<>();
private long lastChangeTime;
private volatile long lastChangeTime;
private SoftReference<ChunkDataPacket> cachedPacket = new SoftReference<>(null);
private ByteBuffer cachedChunkBuffer;
private ByteBuffer cachedLightBuffer;
private long cachedPacketTime;
public DynamicChunk(@NotNull Instance instance, @Nullable Biome[] biomes, int chunkX, int chunkZ) {
@ -117,23 +126,35 @@ public class DynamicChunk extends Chunk {
return lastChangeTime;
}
@NotNull
@Override
public ChunkDataPacket createChunkPacket() {
ChunkDataPacket packet = cachedPacket.get();
if (packet != null && cachedPacketTime == getLastChangeTime()) {
return packet;
public synchronized void sendChunk(@NotNull Player player) {
if (!isLoaded()) return;
final PlayerConnection connection = player.getPlayerConnection();
if (connection instanceof PlayerSocketConnection) {
final long lastChange = getLastChangeTime();
ByteBuffer chunkPacket = cachedChunkBuffer;
ByteBuffer lightPacket = cachedLightBuffer;
if (lastChange > cachedPacketTime || (chunkPacket == null || lightPacket == null)) {
chunkPacket = PacketUtils.allocateTrimmedPacket(createChunkPacket());
lightPacket = PacketUtils.allocateTrimmedPacket(createLightPacket());
this.cachedChunkBuffer = chunkPacket;
this.cachedLightBuffer = lightPacket;
this.cachedPacketTime = lastChange;
}
PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
socketConnection.write(lightPacket);
socketConnection.write(chunkPacket);
} else {
connection.sendPacket(createLightPacket());
connection.sendPacket(createChunkPacket());
}
packet = new ChunkDataPacket(getIdentifier(), getLastChangeTime());
packet.biomes = biomes;
packet.chunkX = chunkX;
packet.chunkZ = chunkZ;
packet.sections = sectionMap.clone(); // TODO deep clone
packet.entries = entries.clone();
}
this.cachedPacketTime = getLastChangeTime();
this.cachedPacket = new SoftReference<>(packet);
return packet;
@Override
public synchronized void sendChunk() {
if (!isLoaded()) return;
sendPacketToViewers(createLightPacket());
sendPacketToViewers(createChunkPacket());
}
@NotNull
@ -153,6 +174,54 @@ public class DynamicChunk extends Chunk {
this.entries.clear();
}
private @NotNull ChunkDataPacket createChunkPacket() {
ChunkDataPacket packet = new ChunkDataPacket();
packet.biomes = biomes;
packet.chunkX = chunkX;
packet.chunkZ = chunkZ;
packet.sections = sectionMap.clone(); // TODO deep clone
packet.entries = entries.clone();
return packet;
}
private @NotNull UpdateLightPacket createLightPacket() {
long skyMask = 0;
long blockMask = 0;
List<byte[]> skyLights = new ArrayList<>();
List<byte[]> blockLights = new ArrayList<>();
UpdateLightPacket updateLightPacket = new UpdateLightPacket();
updateLightPacket.chunkX = getChunkX();
updateLightPacket.chunkZ = getChunkZ();
updateLightPacket.skyLight = skyLights;
updateLightPacket.blockLight = blockLights;
final var sections = getSections();
for (var entry : sections.entrySet()) {
final int index = entry.getKey() + 1;
final Section section = entry.getValue();
final var skyLight = section.getSkyLight();
final var blockLight = section.getBlockLight();
if (!ArrayUtils.empty(skyLight)) {
skyLights.add(skyLight);
skyMask |= 1L << index;
}
if (!ArrayUtils.empty(blockLight)) {
blockLights.add(blockLight);
blockMask |= 1L << index;
}
}
updateLightPacket.skyLightMask = new long[]{skyMask};
updateLightPacket.blockLightMask = new long[]{blockMask};
updateLightPacket.emptySkyLightMask = new long[0];
updateLightPacket.emptyBlockLightMask = new long[0];
return updateLightPacket;
}
private @Nullable Section getOptionalSection(int y) {
final int sectionIndex = ChunkUtils.getSectionAt(y);
return sectionMap.get(sectionIndex);

View File

@ -8,8 +8,6 @@ import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.block.Block;
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.callback.OptionalCallback;
import net.minestom.server.utils.chunk.ChunkCallback;
import net.minestom.server.utils.chunk.ChunkUtils;
@ -225,9 +223,8 @@ public class ChunkBatch implements Batch<ChunkCallback> {
private void updateChunk(@NotNull Instance instance, Chunk chunk, IntSet updatedSections, @Nullable ChunkCallback callback, boolean safeCallback) {
// Refresh chunk for viewers
if (options.shouldSendUpdate()) {
ChunkDataPacket chunkDataPacket = chunk.createChunkPacket();
// TODO update all sections from `updatedSections`
PacketUtils.sendGroupedPacket(chunk.getViewers(), chunkDataPacket);
chunk.sendChunk();
}
if (instance instanceof InstanceContainer) {

View File

@ -1,6 +1,5 @@
package net.minestom.server.item;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import net.minestom.server.instance.block.Block;
import net.minestom.server.item.attribute.ItemAttribute;
@ -13,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jglrxavpok.hephaistos.nbt.NBTCompound;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.Consumer;
@ -36,7 +36,7 @@ public class ItemMeta implements TagReadable, Writeable {
private final NBTCompound nbt;
private String cachedSNBT;
private ByteBuf cachedBuffer;
private ByteBuffer cachedBuffer;
protected ItemMeta(@NotNull ItemMetaBuilder metaBuilder) {
this.damage = metaBuilder.damage;
@ -154,6 +154,5 @@ public class ItemMeta implements TagReadable, Writeable {
this.cachedBuffer = w.getBuffer();
}
writer.write(cachedBuffer);
this.cachedBuffer.resetReaderIndex();
}
}

View File

@ -1,6 +1,5 @@
package net.minestom.server.network;
import io.netty.channel.Channel;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.MinecraftServer;
@ -16,7 +15,7 @@ import net.minestom.server.network.packet.client.login.LoginStartPacket;
import net.minestom.server.network.packet.server.login.LoginSuccessPacket;
import net.minestom.server.network.packet.server.play.DisconnectPacket;
import net.minestom.server.network.packet.server.play.KeepAlivePacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.StringUtils;
import net.minestom.server.utils.async.AsyncUtils;
@ -24,6 +23,7 @@ import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
@ -268,7 +268,7 @@ public final class ConnectionManager {
* @param connection the player connection
* @see PlayerConnection#disconnect() to properly disconnect a player
*/
public void removePlayer(@NotNull PlayerConnection connection) {
public synchronized void removePlayer(@NotNull PlayerConnection connection) {
final Player player = this.connectionPlayerMap.get(connection);
if (player == null)
return;
@ -293,8 +293,8 @@ public final class ConnectionManager {
EventDispatcher.call(asyncPlayerPreLoginEvent);
// Close the player channel if he has been disconnected (kick)
if (!player.isOnline()) {
if (playerConnection instanceof NettyPlayerConnection) {
((NettyPlayerConnection) playerConnection).getChannel().flush();
if (playerConnection instanceof PlayerSocketConnection) {
((PlayerSocketConnection) playerConnection).flush();
}
//playerConnection.disconnect();
return;
@ -312,8 +312,8 @@ public final class ConnectionManager {
}
// Send login success packet
LoginSuccessPacket loginSuccessPacket = new LoginSuccessPacket(player.getUuid(), player.getUsername());
if (playerConnection instanceof NettyPlayerConnection) {
((NettyPlayerConnection) playerConnection).writeAndFlush(loginSuccessPacket);
if (playerConnection instanceof PlayerSocketConnection) {
((PlayerSocketConnection) playerConnection).writeAndFlush(loginSuccessPacket);
} else {
playerConnection.sendPacket(loginSuccessPacket);
}
@ -344,15 +344,18 @@ public final class ConnectionManager {
/**
* Shutdowns the connection manager by kicking all the currently connected players.
*/
public void shutdown() {
public synchronized void shutdown() {
DisconnectPacket disconnectPacket = new DisconnectPacket(shutdownText);
for (Player player : getOnlinePlayers()) {
final PlayerConnection playerConnection = player.getPlayerConnection();
if (playerConnection instanceof NettyPlayerConnection) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection;
final Channel channel = nettyPlayerConnection.getChannel();
channel.writeAndFlush(disconnectPacket);
channel.close();
if (playerConnection instanceof PlayerSocketConnection) {
final PlayerSocketConnection socketConnection = (PlayerSocketConnection) playerConnection;
socketConnection.writeAndFlush(disconnectPacket);
try {
socketConnection.getChannel().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
this.players.clear();

View File

@ -1,27 +1,22 @@
package net.minestom.server.network;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.network.packet.client.ClientPlayPacket;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.packet.client.handler.ClientLoginPacketsHandler;
import net.minestom.server.network.packet.client.handler.ClientPlayPacketsHandler;
import net.minestom.server.network.packet.client.handler.ClientStatusPacketsHandler;
import net.minestom.server.network.packet.client.handshake.HandshakePacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.Readable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.ByteBuffer;
/**
* Responsible for processing client packets.
@ -34,11 +29,8 @@ import java.util.concurrent.ConcurrentHashMap;
* the same meaning as it is a login or play packet).
*/
public final class PacketProcessor {
private final static Logger LOGGER = LoggerFactory.getLogger(PacketProcessor.class);
private final Map<ChannelHandlerContext, PlayerConnection> connectionPlayerConnectionMap = new ConcurrentHashMap<>();
// Protocols state
private final ClientStatusPacketsHandler statusPacketsHandler;
private final ClientLoginPacketsHandler loginPacketsHandler;
@ -50,33 +42,13 @@ public final class PacketProcessor {
this.playPacketsHandler = new ClientPlayPacketsHandler();
}
public void process(@NotNull ChannelHandlerContext context, @NotNull InboundPacket packet) {
final SocketChannel socketChannel = (SocketChannel) context.channel();
// Create the netty player connection object if not existing
PlayerConnection playerConnection = connectionPlayerConnectionMap.get(context);
if (playerConnection == null) {
// Should never happen
context.close();
return;
}
// Prevent the client from sending packets when disconnected (kick)
if (!playerConnection.isOnline() || !socketChannel.isActive()) {
playerConnection.disconnect();
return;
}
// Increment packet count (checked in PlayerConnection#update)
public void process(@NotNull PlayerSocketConnection playerConnection, int packetId, ByteBuffer body) {
if (MinecraftServer.getRateLimit() > 0) {
// Increment packet count (checked in PlayerConnection#update)
playerConnection.getPacketCounter().incrementAndGet();
}
BinaryReader binaryReader = new BinaryReader(body);
final ConnectionState connectionState = playerConnection.getConnectionState();
final int packetId = packet.getPacketId();
BinaryReader binaryReader = new BinaryReader(packet.getBody());
if (connectionState == ConnectionState.UNKNOWN) {
// Should be handshake packet
if (packetId == 0) {
@ -86,7 +58,6 @@ public final class PacketProcessor {
}
return;
}
switch (connectionState) {
case PLAY:
final Player player = playerConnection.getPlayer();
@ -108,34 +79,13 @@ public final class PacketProcessor {
}
}
/**
* Retrieves a player connection from its channel.
*
* @param context the connection context
* @return the connection of this channel, null if not found
*/
@Nullable
public PlayerConnection getPlayerConnection(ChannelHandlerContext context) {
return connectionPlayerConnectionMap.get(context);
}
public void createPlayerConnection(@NotNull ChannelHandlerContext context) {
final PlayerConnection playerConnection = new NettyPlayerConnection((SocketChannel) context.channel());
connectionPlayerConnectionMap.put(context, playerConnection);
}
public PlayerConnection removePlayerConnection(@NotNull ChannelHandlerContext context) {
return connectionPlayerConnectionMap.remove(context);
}
/**
* Gets the handler for client status packets.
*
* @return the status packets handler
* @see <a href="https://wiki.vg/Protocol#Status">Status packets</a>
*/
@NotNull
public ClientStatusPacketsHandler getStatusPacketsHandler() {
public @NotNull ClientStatusPacketsHandler getStatusPacketsHandler() {
return statusPacketsHandler;
}
@ -145,8 +95,7 @@ public final class PacketProcessor {
* @return the status login handler
* @see <a href="https://wiki.vg/Protocol#Login">Login packets</a>
*/
@NotNull
public ClientLoginPacketsHandler getLoginPacketsHandler() {
public @NotNull ClientLoginPacketsHandler getLoginPacketsHandler() {
return loginPacketsHandler;
}
@ -156,8 +105,7 @@ public final class PacketProcessor {
* @return the play packets handler
* @see <a href="https://wiki.vg/Protocol#Play">Play packets</a>
*/
@NotNull
public ClientPlayPacketsHandler getPlayPacketsHandler() {
public @NotNull ClientPlayPacketsHandler getPlayPacketsHandler() {
return playPacketsHandler;
}
@ -170,20 +118,16 @@ public final class PacketProcessor {
*/
private void safeRead(@NotNull PlayerConnection connection, @NotNull Readable readable, @NotNull BinaryReader reader) {
final int readableBytes = reader.available();
// Check if there is anything to read
if (readableBytes == 0) {
return;
}
try {
readable.read(reader);
} catch (Exception e) {
final Player player = connection.getPlayer();
final String username = player != null ? player.getUsername() : "null";
LOGGER.warn("Connection {} ({}) sent an unexpected packet.",
connection.getRemoteAddress(),
username);
LOGGER.warn("Connection {} ({}) sent an unexpected packet.", connection.getRemoteAddress(), username);
MinecraftServer.getExceptionManager().handleException(e);
}
}

View File

@ -1,202 +0,0 @@
package net.minestom.server.network.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueEventLoopGroup;
import io.netty.channel.kqueue.KQueueServerSocketChannel;
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 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.*;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
@ApiStatus.Internal
public final class NettyServer {
public static final Logger LOGGER = LoggerFactory.getLogger(NettyServer.class);
public static final int BUFFER_SIZE = Integer.getInteger("minestom.channel-buffer-size", 65535);
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
1 << 21);
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 boolean initialized = false;
private final PacketProcessor packetProcessor;
private EventLoopGroup boss, worker;
private ServerBootstrap bootstrap;
private ServerSocketChannel serverChannel;
private String address;
private int port;
public NettyServer(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
/**
* Inits the server by choosing which transport layer to use, number of threads, pipeline order, etc...
* <p>
* Called just before {@link #start(String, int)}.
*/
public void init() {
Check.stateCondition(initialized, "Netty server has already been initialized!");
this.initialized = true;
Class<? extends ServerChannel> channel;
final int workerThreadCount = MinecraftServer.getNettyThreadCount();
// Find boss/worker event group
{
if (Epoll.isAvailable()) {
boss = new EpollEventLoopGroup(2);
worker = new EpollEventLoopGroup(workerThreadCount);
channel = EpollServerSocketChannel.class;
LOGGER.info("Using epoll");
} else if (KQueue.isAvailable()) {
boss = new KQueueEventLoopGroup(2);
worker = new KQueueEventLoopGroup(workerThreadCount);
channel = KQueueServerSocketChannel.class;
LOGGER.info("Using kqueue");
} else {
boss = new NioEventLoopGroup(2);
worker = new NioEventLoopGroup(workerThreadCount);
channel = NioServerSocketChannel.class;
LOGGER.info("Using NIO");
}
}
bootstrap = new ServerBootstrap()
.group(boss, worker)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.channel(channel);
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_KEEPALIVE, true);
config.setOption(ChannelOption.SO_SNDBUF, BUFFER_SIZE);
config.setAllocator(ByteBufAllocator.DEFAULT);
ChannelPipeline pipeline = ch.pipeline();
// 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_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 buffer
pipeline.addLast(FRAMER_HANDLER_NAME, new PacketFramer(packetProcessor));
// Reads buffer and create inbound packet
pipeline.addLast(DECODER_HANDLER_NAME, new PacketDecoder());
// Writes packet to buffer
pipeline.addLast(ENCODER_HANDLER_NAME, new PacketEncoder());
pipeline.addLast(CLIENT_CHANNEL_NAME, new ClientChannel(packetProcessor));
}
});
}
/**
* 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) {
this.address = address;
this.port = port;
// Bind address
try {
ChannelFuture cf = bootstrap.bind(new InetSocketAddress(address, port)).sync();
if (!cf.isSuccess()) {
throw new IllegalStateException("Unable to bind server at " + address + ":" + port);
}
this.serverChannel = (ServerSocketChannel) cf.channel();
} catch (InterruptedException ex) {
MinecraftServer.getExceptionManager().handleException(ex);
}
}
public ServerSocketChannel getServerChannel() {
return serverChannel;
}
/**
* Gets the address of the server.
*
* @return the server address, null if the address isn't bound yet
*/
@Nullable
public String getAddress() {
return address;
}
/**
* Gets the port used by the server.
*
* @return the server port, 0 if the address isn't bound yet
*/
public int getPort() {
return port;
}
/**
* Stops the server.
*/
public void stop() {
try {
this.boss.shutdownGracefully().sync();
this.worker.shutdownGracefully().sync();
this.serverChannel.closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View File

@ -1,90 +0,0 @@
package net.minestom.server.network.netty.channel;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ClientChannel extends SimpleChannelInboundHandler<InboundPacket> {
private final static Logger LOGGER = LoggerFactory.getLogger(ClientChannel.class);
private final static ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager();
private final PacketProcessor packetProcessor;
public ClientChannel(@NotNull PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
@Override
public void channelActive(@NotNull ChannelHandlerContext ctx) {
//System.out.println("CONNECTION");
packetProcessor.createPlayerConnection(ctx);
}
@Override
public void channelRead0(ChannelHandlerContext ctx, InboundPacket packet) {
try {
packetProcessor.process(ctx, packet);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
} finally {
// Check remaining
final ByteBuf body = packet.getBody();
final int packetId = packet.getPacketId();
final int availableBytes = body.readableBytes();
if (availableBytes > 0) {
final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
LOGGER.warn("WARNING: Packet 0x{} not fully read ({} bytes left), {}",
Integer.toHexString(packetId),
availableBytes,
playerConnection);
body.skipBytes(availableBytes);
}
}
}
@Override
public void channelInactive(@NotNull ChannelHandlerContext ctx) {
PlayerConnection playerConnection = packetProcessor.removePlayerConnection(ctx);
if (playerConnection != null) {
// Remove the connection
playerConnection.refreshOnline(false);
Player player = playerConnection.getPlayer();
if (player != null) {
player.remove();
CONNECTION_MANAGER.removePlayer(playerConnection);
}
// Release tick buffer
if (playerConnection instanceof NettyPlayerConnection) {
((NettyPlayerConnection) playerConnection).releaseTickBuffer();
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (!ctx.channel().isActive()) {
return;
}
if (MinecraftServer.shouldProcessNettyErrors()) {
MinecraftServer.getExceptionManager().handleException(cause);
}
ctx.close();
}
}

View File

@ -1,19 +0,0 @@
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) {
}
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, FramedPacket msg, boolean preferDirect) {
return msg.getBody().retainedSlice();
}
}

View File

@ -1,184 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.ping.ServerListPingType;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object object) {
final ByteBuf buf = (ByteBuf) object;
if (this.buf != null) {
try {
handle1_6(ctx, buf);
} finally {
buf.release();
}
return;
}
buf.markReaderIndex();
boolean flag = true;
try {
if (buf.readUnsignedByte() == 0xFE) {
int length = buf.readableBytes();
switch (length) {
case 0:
if (trySendResponse(ServerListPingType.LEGACY_UNVERSIONED, ctx)) return;
break;
case 1:
if (buf.readUnsignedByte() != 1) return;
if (trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx)) return;
break;
default:
if (buf.readUnsignedByte() != 0x01 || buf.readUnsignedByte() != 0xFA) return;
handle1_6(ctx, buf);
break;
}
buf.release();
flag = false;
}
} finally {
if (flag) {
buf.resetReaderIndex();
ctx.channel().pipeline().remove("legacy-ping");
ctx.fireChannelRead(object);
}
}
}
private void handle1_6(ChannelHandlerContext ctx, ByteBuf part) {
ByteBuf buf = this.buf;
if (buf == null) {
this.buf = buf = ctx.alloc().buffer();
buf.markReaderIndex();
} else {
buf.resetReaderIndex();
}
buf.writeBytes(part);
if (!buf.isReadable(Short.BYTES + Short.BYTES + Byte.BYTES + Short.BYTES + Integer.BYTES)) {
return;
}
final String s = readLegacyString(buf);
if (s == null) {
return;
}
if (!s.equals("MC|PingHost")) {
removeHandler(ctx);
return;
}
if (!buf.isReadable(Short.BYTES) || !buf.isReadable(buf.readShort())) {
return;
}
int protocolVersion = buf.readByte();
if (readLegacyString(buf) == null) {
removeHandler(ctx);
return;
}
buf.skipBytes(4); // port
if (buf.isReadable()) {
removeHandler(ctx);
return;
}
buf.release();
this.buf = null;
trySendResponse(ServerListPingType.LEGACY_VERSIONED, ctx);
}
private void removeHandler(ChannelHandlerContext ctx) {
ByteBuf buf = this.buf;
this.buf = null;
buf.resetReaderIndex();
ctx.pipeline().remove(this);
ctx.fireChannelRead(buf);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
if (this.buf != null) {
this.buf.release();
this.buf = null;
}
}
/**
* Calls a {@link ServerListPingEvent} and sends the response, if the event was not cancelled.
*
* @param version the version
* @param ctx the context
* @return {@code true} if the response was cancelled, {@code false} otherwise
*/
private static boolean trySendResponse(@NotNull ServerListPingType version, @NotNull ChannelHandlerContext ctx) {
final ServerListPingEvent event = new ServerListPingEvent(version);
EventDispatcher.call(event);
if (event.isCancelled()) {
return true;
} else {
// get the response string
String s = version.getPingResponse(event.getResponseData());
// create the buffer
ByteBuf response = Unpooled.buffer();
response.writeByte(255);
final char[] chars = s.toCharArray();
response.writeShort(chars.length);
for (char c : chars) {
response.writeChar(c);
}
// write the buffer
ctx.pipeline().firstContext().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
return false;
}
}
private static String readLegacyString(ByteBuf buf) {
int size = buf.readShort() * Character.BYTES;
if (!buf.isReadable(size)) {
return null;
}
final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
buf.skipBytes(size);
return result;
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright (2020) [artem]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
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;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
public class PacketCompressor extends ByteToMessageCodec<ByteBuf> {
private final static int MAX_SIZE = 2097152;
private final int threshold;
private final Deflater deflater = new Deflater();
private final Inflater inflater = new Inflater();
public PacketCompressor(int threshold) {
this.threshold = threshold;
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
PacketUtils.compressBuffer(deflater, from, to);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() != 0) {
final int claimedUncompressedSize = Utils.readVarInt(in);
if (claimedUncompressedSize == 0) {
out.add(in.readRetainedSlice(in.readableBytes()));
} else {
if (claimedUncompressedSize < this.threshold) {
throw new DecoderException("Badly compressed packet - size of " + claimedUncompressedSize + " is below server threshold of " + this.threshold);
}
if (claimedUncompressedSize > MAX_SIZE) {
throw new DecoderException("Badly compressed packet - size of " + claimedUncompressedSize + " is larger than protocol maximum of " + MAX_SIZE);
}
// TODO optimize to do not initialize arrays each time
byte[] input = new byte[in.readableBytes()];
in.readBytes(input);
inflater.setInput(input);
byte[] output = new byte[claimedUncompressedSize];
inflater.inflate(output);
inflater.reset();
out.add(Unpooled.wrappedBuffer(output));
}
}
}
}

View File

@ -1,20 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import net.minestom.server.network.netty.packet.InboundPacket;
import net.minestom.server.utils.Utils;
import java.util.List;
public class PacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
if (buf.readableBytes() > 0) {
final int packetId = Utils.readVarInt(buf);
list.add(new InboundPacket(packetId, buf));
}
}
}

View File

@ -1,16 +0,0 @@
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.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils;
public class PacketEncoder extends MessageToByteEncoder<ServerPacket> {
@Override
protected void encode(ChannelHandlerContext ctx, ServerPacket packet, ByteBuf buf) {
PacketUtils.writePacket(buf, packet);
}
}

View File

@ -1,74 +0,0 @@
package net.minestom.server.network.netty.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
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;
import java.util.List;
public class PacketFramer extends ByteToMessageCodec<ByteBuf> {
public final static Logger LOGGER = LoggerFactory.getLogger(PacketFramer.class);
private final PacketProcessor packetProcessor;
public PacketFramer(PacketProcessor packetProcessor) {
this.packetProcessor = packetProcessor;
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf from, ByteBuf to) {
PacketUtils.frameBuffer(from, to);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) {
buf.markReaderIndex();
for (int i = 0; i < 3; ++i) {
if (!buf.isReadable()) {
buf.resetReaderIndex();
return;
}
final byte b = buf.readByte();
if (b >= 0) {
buf.resetReaderIndex();
final int packetSize = Utils.readVarInt(buf);
// Max packet size check
if (packetSize >= MinecraftServer.getMaxPacketSize()) {
final PlayerConnection playerConnection = packetProcessor.getPlayerConnection(ctx);
if (playerConnection != null) {
final String identifier = playerConnection.getIdentifier();
LOGGER.warn("An user ({}) sent a packet over the maximum size ({})",
identifier, packetSize);
} else {
LOGGER.warn("An unregistered user sent a packet over the maximum size ({})", packetSize);
}
ctx.close();
}
if (buf.readableBytes() < packetSize) {
buf.resetReaderIndex();
return;
}
out.add(buf.readRetainedSlice(packetSize));
return;
}
}
throw new CorruptedFrameException("length wider than 21-bit");
}
}

View File

@ -1,22 +0,0 @@
package net.minestom.server.network.netty.packet;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
/**
* Represents a packet which is already framed. (packet id+payload) + optional compression
* Can be used if you want to send the exact same buffer to multiple clients without processing it more than once.
*/
public class FramedPacket {
private final ByteBuf body;
public FramedPacket(@NotNull ByteBuf body) {
this.body = body;
}
@NotNull
public ByteBuf getBody() {
return body;
}
}

View File

@ -1,24 +0,0 @@
package net.minestom.server.network.netty.packet;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
public class InboundPacket {
private final int packetId;
private final ByteBuf body;
public InboundPacket(int id, @NotNull ByteBuf body) {
this.packetId = id;
this.body = body;
}
public int getPacketId() {
return packetId;
}
@NotNull
public ByteBuf getBody() {
return body;
}
}

View File

@ -0,0 +1,27 @@
package net.minestom.server.network.packet;
import org.jetbrains.annotations.NotNull;
import java.nio.ByteBuffer;
/**
* Represents a packet which is already framed. (packet id+payload) + optional compression
* Can be used if you want to send the exact same buffer to multiple clients without processing it more than once.
*/
public final class FramedPacket {
private final int packetId;
private final ByteBuffer body;
public FramedPacket(int packetId, @NotNull ByteBuffer body) {
this.packetId = packetId;
this.body = body;
}
public int packetId() {
return packetId;
}
public @NotNull ByteBuffer body() {
return body;
}
}

View File

@ -4,10 +4,8 @@ import net.minestom.server.network.packet.client.status.PingPacket;
import net.minestom.server.network.packet.client.status.StatusRequestPacket;
public class ClientStatusPacketsHandler extends ClientPacketsHandler {
public ClientStatusPacketsHandler() {
register(0x00, StatusRequestPacket::new);
register(0x01, PingPacket::new);
}
}

View File

@ -8,7 +8,7 @@ import net.minestom.server.extras.bungee.BungeeCordProxy;
import net.minestom.server.network.ConnectionState;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
@ -54,8 +54,8 @@ public class HandshakePacket implements ClientPreplayPacket {
public void process(@NotNull PlayerConnection connection) {
// Bungee support (IP forwarding)
if (BungeeCordProxy.isEnabled() && connection instanceof NettyPlayerConnection) {
NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
if (BungeeCordProxy.isEnabled() && connection instanceof PlayerSocketConnection) {
PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
if (serverAddress != null) {
final String[] split = serverAddress.split("\00");
@ -65,7 +65,7 @@ public class HandshakePacket implements ClientPreplayPacket {
final SocketAddress socketAddress = new java.net.InetSocketAddress(split[1],
((java.net.InetSocketAddress) connection.getRemoteAddress()).getPort());
nettyPlayerConnection.setRemoteAddress(socketAddress);
socketConnection.setRemoteAddress(socketAddress);
UUID playerUuid = UUID.fromString(
split[2]
@ -79,11 +79,11 @@ public class HandshakePacket implements ClientPreplayPacket {
playerSkin = BungeeCordProxy.readSkin(split[3]);
}
nettyPlayerConnection.UNSAFE_setBungeeUuid(playerUuid);
nettyPlayerConnection.UNSAFE_setBungeeSkin(playerSkin);
socketConnection.UNSAFE_setBungeeUuid(playerUuid);
socketConnection.UNSAFE_setBungeeSkin(playerSkin);
} else {
nettyPlayerConnection.sendPacket(new LoginDisconnectPacket(INVALID_BUNGEE_FORWARDING));
nettyPlayerConnection.disconnect();
socketConnection.sendPacket(new LoginDisconnectPacket(INVALID_BUNGEE_FORWARDING));
socketConnection.disconnect();
return;
}
} else {
@ -92,9 +92,9 @@ public class HandshakePacket implements ClientPreplayPacket {
}
}
if (connection instanceof NettyPlayerConnection) {
if (connection instanceof PlayerSocketConnection) {
// Give to the connection the server info that the client used
((NettyPlayerConnection) connection).refreshServerInformation(serverAddress, serverPort, protocolVersion);
((PlayerSocketConnection) connection).refreshServerInformation(serverAddress, serverPort, protocolVersion);
}
switch (nextState) {

View File

@ -7,8 +7,8 @@ import net.minestom.server.data.type.array.ByteArrayData;
import net.minestom.server.extras.MojangAuth;
import net.minestom.server.extras.mojangAuth.MojangCrypt;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
@ -35,31 +35,26 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
@Override
public void process(@NotNull PlayerConnection connection) {
// Encryption is only support for netty connection
if (!(connection instanceof NettyPlayerConnection)) {
// Encryption is only support for socket connection
if (!(connection instanceof PlayerSocketConnection)) {
return;
}
final NettyPlayerConnection nettyConnection = (NettyPlayerConnection) connection;
final PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
AsyncUtils.runAsync(() -> {
try {
final String loginUsername = nettyConnection.getLoginUsername();
if (!Arrays.equals(nettyConnection.getNonce(), getNonce())) {
final String loginUsername = socketConnection.getLoginUsername();
if (!Arrays.equals(socketConnection.getNonce(), getNonce())) {
MinecraftServer.LOGGER.error("{} tried to login with an invalid nonce!", loginUsername);
return;
}
if (loginUsername != null && !loginUsername.isEmpty()) {
final byte[] digestedData = MojangCrypt.digestData("", MojangAuth.getKeyPair().getPublic(), getSecretKey());
if (digestedData == null) {
// Incorrect key, probably because of the client
MinecraftServer.LOGGER.error("Connection {} failed initializing encryption.", nettyConnection.getRemoteAddress());
MinecraftServer.LOGGER.error("Connection {} failed initializing encryption.", socketConnection.getRemoteAddress());
connection.disconnect();
return;
}
// Query Mojang's sessionserver.
final String serverId = new BigInteger(digestedData).toString(16);
InputStream gameProfileStream = new URL(
@ -70,7 +65,7 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
).openStream();
final JsonObject gameProfile = GSON.fromJson(new InputStreamReader(gameProfileStream), JsonObject.class);
nettyConnection.setEncryptionKey(getSecretKey());
socketConnection.setEncryptionKey(getSecretKey());
UUID profileUUID = UUID.fromString(gameProfile.get("id").getAsString().replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"));
String profileName = gameProfile.get("name").getAsString();
@ -85,8 +80,8 @@ public class EncryptionResponsePacket implements ClientPreplayPacket {
@Override
public void read(@NotNull BinaryReader reader) {
sharedSecret = ByteArrayData.decodeByteArray(reader);
verifyToken = ByteArrayData.decodeByteArray(reader);
this.sharedSecret = ByteArrayData.decodeByteArray(reader);
this.verifyToken = ByteArrayData.decodeByteArray(reader);
}
@Override

View File

@ -9,7 +9,7 @@ import net.minestom.server.extras.velocity.VelocityProxy;
import net.minestom.server.network.ConnectionManager;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
@ -34,9 +34,9 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
public void process(@NotNull PlayerConnection connection) {
// Proxy support
if (connection instanceof NettyPlayerConnection) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
final String channel = nettyPlayerConnection.getPluginRequestChannel(messageId);
if (connection instanceof PlayerSocketConnection) {
final PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
final String channel = socketConnection.getPluginRequestChannel(messageId);
if (channel != null) {
boolean success = false;
@ -68,13 +68,13 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
if (success) {
if (socketAddress != null) {
nettyPlayerConnection.setRemoteAddress(socketAddress);
socketConnection.setRemoteAddress(socketAddress);
}
if (playerUsername != null) {
nettyPlayerConnection.UNSAFE_setLoginUsername(playerUsername);
socketConnection.UNSAFE_setLoginUsername(playerUsername);
}
final String username = nettyPlayerConnection.getLoginUsername();
final String username = socketConnection.getLoginUsername();
final UUID uuid = playerUuid != null ?
playerUuid : CONNECTION_MANAGER.getPlayerConnectionUuid(connection, username);
@ -82,7 +82,7 @@ public class LoginPluginResponsePacket implements ClientPreplayPacket {
player.setSkin(playerSkin);
} else {
LoginDisconnectPacket disconnectPacket = new LoginDisconnectPacket(INVALID_PROXY_RESPONSE);
nettyPlayerConnection.sendPacket(disconnectPacket);
socketConnection.sendPacket(disconnectPacket);
}
}

View File

@ -12,8 +12,8 @@ import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.packet.server.login.EncryptionRequestPacket;
import net.minestom.server.network.packet.server.login.LoginDisconnectPacket;
import net.minestom.server.network.packet.server.login.LoginPluginRequestPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull;
@ -29,70 +29,58 @@ public class LoginStartPacket implements ClientPreplayPacket {
@Override
public void process(@NotNull PlayerConnection connection) {
final boolean isNettyClient = connection instanceof NettyPlayerConnection;
final boolean isSocketConnection = connection instanceof PlayerSocketConnection;
// Cache the login username and start compression if enabled
if (isNettyClient) {
NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
nettyPlayerConnection.UNSAFE_setLoginUsername(username);
if (isSocketConnection) {
PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
socketConnection.UNSAFE_setLoginUsername(username);
// Compression
final int threshold = MinecraftServer.getCompressionThreshold();
if (threshold > 0) {
nettyPlayerConnection.startCompression();
socketConnection.startCompression();
}
}
// Proxy support (only for socket clients)
if (isSocketConnection) {
final PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
// Velocity support
if (VelocityProxy.isEnabled()) {
final int messageId = ThreadLocalRandom.current().nextInt();
final String channel = VelocityProxy.PLAYER_INFO_CHANNEL;
// Important in order to retrieve the channel in the response packet
socketConnection.addPluginRequestEntry(messageId, channel);
LoginPluginRequestPacket loginPluginRequestPacket = new LoginPluginRequestPacket();
loginPluginRequestPacket.messageId = messageId;
loginPluginRequestPacket.channel = channel;
loginPluginRequestPacket.data = null;
connection.sendPacket(loginPluginRequestPacket);
return;
}
}
// Proxy support (only for netty clients)
if (isNettyClient) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
{
// Velocity support
if (VelocityProxy.isEnabled()) {
final int messageId = ThreadLocalRandom.current().nextInt();
final String channel = VelocityProxy.PLAYER_INFO_CHANNEL;
// Important in order to retrieve the channel in the response packet
nettyPlayerConnection.addPluginRequestEntry(messageId, channel);
LoginPluginRequestPacket loginPluginRequestPacket = new LoginPluginRequestPacket();
loginPluginRequestPacket.messageId = messageId;
loginPluginRequestPacket.channel = channel;
loginPluginRequestPacket.data = null;
connection.sendPacket(loginPluginRequestPacket);
return;
}
}
}
if (MojangAuth.isEnabled() && isNettyClient) {
if (MojangAuth.isEnabled() && isSocketConnection) {
// Mojang auth
if (CONNECTION_MANAGER.getPlayer(username) != null) {
connection.sendPacket(new LoginDisconnectPacket(ALREADY_CONNECTED));
connection.disconnect();
return;
}
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) connection;
nettyPlayerConnection.setConnectionState(ConnectionState.LOGIN);
EncryptionRequestPacket encryptionRequestPacket = new EncryptionRequestPacket(nettyPlayerConnection);
nettyPlayerConnection.sendPacket(encryptionRequestPacket);
final PlayerSocketConnection socketConnection = (PlayerSocketConnection) connection;
socketConnection.setConnectionState(ConnectionState.LOGIN);
EncryptionRequestPacket encryptionRequestPacket = new EncryptionRequestPacket(socketConnection);
socketConnection.sendPacket(encryptionRequestPacket);
} else {
final boolean bungee = BungeeCordProxy.isEnabled();
// Offline
final UUID playerUuid = bungee && isNettyClient ?
((NettyPlayerConnection) connection).getBungeeUuid() :
final UUID playerUuid = bungee && isSocketConnection ?
((PlayerSocketConnection) connection).getBungeeUuid() :
CONNECTION_MANAGER.getPlayerConnectionUuid(connection, username);
Player player = CONNECTION_MANAGER.startPlayState(connection, playerUuid, username, true);
if (bungee && isNettyClient) {
player.setSkin(((NettyPlayerConnection) connection).getBungeeSkin());
if (bungee && isSocketConnection) {
player.setSkin(((PlayerSocketConnection) connection).getBungeeSkin());
}
}
}
@ -104,7 +92,7 @@ public class LoginStartPacket implements ClientPreplayPacket {
@Override
public void write(@NotNull BinaryWriter writer) {
if(username.length() > 16)
if (username.length() > 16)
throw new IllegalArgumentException("Username is not allowed to be longer than 16 characters");
writer.writeSizedString(username);
}

View File

@ -16,12 +16,8 @@ public class StatusRequestPacket implements ClientPreplayPacket {
public void process(@NotNull PlayerConnection connection) {
final ServerListPingType pingVersion = ServerListPingType.fromModernProtocolVersion(connection.getProtocolVersion());
final ServerListPingEvent statusRequestEvent = new ServerListPingEvent(connection, pingVersion);
EventDispatcher.callCancellable(statusRequestEvent, () -> {
final ResponsePacket responsePacket = new ResponsePacket();
responsePacket.jsonResponse = pingVersion.getPingResponse(statusRequestEvent.getResponseData());
connection.sendPacket(responsePacket);
});
EventDispatcher.callCancellable(statusRequestEvent, () ->
connection.sendPacket(new ResponsePacket(pingVersion.getPingResponse(statusRequestEvent.getResponseData()))));
}
@Override

View File

@ -7,7 +7,11 @@ import org.jetbrains.annotations.NotNull;
public class ResponsePacket implements ServerPacket {
public String jsonResponse = "";
public String jsonResponse;
public ResponsePacket(String jsonResponse) {
this.jsonResponse = jsonResponse;
}
@Override
public void write(@NotNull BinaryWriter writer) {

View File

@ -4,7 +4,7 @@ import net.minestom.server.data.type.array.ByteArrayData;
import net.minestom.server.extras.MojangAuth;
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.PlayerSocketConnection;
import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull;
@ -16,7 +16,7 @@ public class EncryptionRequestPacket implements ServerPacket {
public byte[] publicKey;
public byte[] nonce = new byte[4];
public EncryptionRequestPacket(NettyPlayerConnection connection) {
public EncryptionRequestPacket(PlayerSocketConnection connection) {
ThreadLocalRandom.current().nextBytes(nonce);
connection.setNonce(nonce);
}

View File

@ -1,7 +1,5 @@
package net.minestom.server.network.packet.server.play;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2LongRBTreeMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
@ -15,22 +13,17 @@ import net.minestom.server.tag.Tag;
import net.minestom.server.utils.Utils;
import net.minestom.server.utils.binary.BinaryReader;
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 org.jglrxavpok.hephaistos.nbt.NBTException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class ChunkDataPacket implements ServerPacket, CacheablePacket {
public static final TemporaryPacketCache CACHE = new TemporaryPacketCache(5, TimeUnit.MINUTES);
public class ChunkDataPacket implements ServerPacket {
public Biome[] biomes;
public int chunkX, chunkZ;
@ -42,22 +35,13 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
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 final UUID identifier;
private final long timestamp;
/**
* Heightmaps NBT, as read from raw packet data.
* Only filled by #read, and unused at the moment.
*/
public NBTCompound heightmapsNBT;
private ChunkDataPacket() {
this(new UUID(0, 0), 0);
}
public ChunkDataPacket(@Nullable UUID identifier, long timestamp) {
this.identifier = identifier;
this.timestamp = timestamp;
public ChunkDataPacket() {
}
@Override
@ -65,7 +49,7 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
writer.writeInt(chunkX);
writer.writeInt(chunkZ);
ByteBuf blocks = Unpooled.buffer(MAX_BUFFER_SIZE);
ByteBuffer blocks = ByteBuffer.allocate(MAX_BUFFER_SIZE);
Int2LongRBTreeMap maskMap = new Int2LongRBTreeMap();
@ -120,9 +104,8 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
}
// Data
writer.writeVarInt(blocks.writerIndex());
writer.writeVarInt(blocks.position());
writer.write(blocks);
blocks.release();
// Block entities
if (entries == null || entries.isEmpty()) {
@ -244,19 +227,4 @@ public class ChunkDataPacket implements ServerPacket, CacheablePacket {
public int getId() {
return ServerPacketIdentifier.CHUNK_DATA;
}
@Override
public @NotNull TemporaryPacketCache getCache() {
return CACHE;
}
@Override
public UUID getIdentifier() {
return identifier;
}
@Override
public long getTimestamp() {
return timestamp;
}
}

View File

@ -4,19 +4,12 @@ import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.ServerPacketIdentifier;
import net.minestom.server.utils.binary.BinaryReader;
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.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class UpdateLightPacket implements ServerPacket, CacheablePacket {
public static final TemporaryPacketCache CACHE = new TemporaryPacketCache(5, TimeUnit.MINUTES);
public class UpdateLightPacket implements ServerPacket {
public int chunkX;
public int chunkZ;
@ -32,21 +25,10 @@ public class UpdateLightPacket implements ServerPacket, CacheablePacket {
public List<byte[]> skyLight = new ArrayList<>();
public List<byte[]> blockLight = new ArrayList<>();
// Cacheable data
private final UUID identifier;
private final long timestamp;
/**
* Default constructor, required for reflection operations.
* This one will make a packet that is not meant to be cached
*/
public UpdateLightPacket() {
this(UUID.randomUUID(), Long.MAX_VALUE);
}
public UpdateLightPacket(@Nullable UUID identifier, long timestamp) {
this.identifier = identifier;
this.timestamp = timestamp;
}
@Override
@ -118,19 +100,4 @@ public class UpdateLightPacket implements ServerPacket, CacheablePacket {
public int getId() {
return ServerPacketIdentifier.UPDATE_LIGHT;
}
@Override
public @NotNull TemporaryPacketCache getCache() {
return CACHE;
}
@Override
public UUID getIdentifier() {
return identifier;
}
@Override
public long getTimestamp() {
return timestamp;
}
}

View File

@ -21,7 +21,6 @@ import java.util.concurrent.atomic.AtomicInteger;
* It can be extended to create a new kind of player (NPC for instance).
*/
public abstract class PlayerConnection {
protected static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
private Player player;
@ -63,8 +62,7 @@ public abstract class PlayerConnection {
}
}
@NotNull
public AtomicInteger getPacketCounter() {
public @NotNull AtomicInteger getPacketCounter() {
return packetCounter;
}
@ -74,8 +72,7 @@ public abstract class PlayerConnection {
*
* @return this connection identifier
*/
@NotNull
public String getIdentifier() {
public @NotNull String getIdentifier() {
final Player player = getPlayer();
return player != null ?
player.getUsername() :
@ -114,9 +111,7 @@ public abstract class PlayerConnection {
*
* @return the remote address
*/
@NotNull
public abstract SocketAddress getRemoteAddress();
public abstract @NotNull SocketAddress getRemoteAddress();
/**
* Gets protocol version of client.
@ -135,7 +130,7 @@ public abstract class PlayerConnection {
* @return the server address used
*/
public @Nullable String getServerAddress() {
return MinecraftServer.getNettyServer().getAddress();
return MinecraftServer.getServer().getAddress();
}
@ -147,7 +142,7 @@ public abstract class PlayerConnection {
* @return the server port used
*/
public int getServerPort() {
return MinecraftServer.getNettyServer().getPort();
return MinecraftServer.getServer().getPort();
}
/**
@ -160,8 +155,7 @@ public abstract class PlayerConnection {
*
* @return the player, can be null if not initialized yet
*/
@Nullable
public Player getPlayer() {
public @Nullable Player getPlayer() {
return player;
}
@ -198,8 +192,7 @@ public abstract class PlayerConnection {
*
* @return the client connection state
*/
@NotNull
public ConnectionState getConnectionState() {
public @NotNull ConnectionState getConnectionState() {
return connectionState;
}

View File

@ -1,53 +1,58 @@
package net.minestom.server.network.player;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.socket.SocketChannel;
import net.kyori.adventure.translation.GlobalTranslator;
import net.minestom.server.MinecraftServer;
import net.minestom.server.adventure.MinestomAdventure;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerSkin;
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.PacketProcessor;
import net.minestom.server.network.packet.FramedPacket;
import net.minestom.server.network.packet.server.ComponentHoldingServerPacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.packet.server.login.SetCompressionPacket;
import net.minestom.server.utils.BufUtils;
import net.minestom.server.network.socket.Server;
import net.minestom.server.network.socket.Worker;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.cache.CacheablePacket;
import net.minestom.server.utils.binary.BinaryBuffer;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.DataFormatException;
/**
* Represents a networking connection with Netty.
* Represents a socket connection.
* <p>
* It is the implementation used for all network client.
*/
public class NettyPlayerConnection extends PlayerConnection {
@ApiStatus.Internal
public class PlayerSocketConnection extends PlayerConnection {
private final Worker worker;
private final SocketChannel channel;
private SocketAddress remoteAddress;
private boolean encrypted = false;
private boolean compressed = false;
private volatile boolean encrypted = false;
private volatile boolean compressed = false;
//Could be null. Only used for Mojang Auth
private byte[] nonce = new byte[4];
private Cipher decryptCipher;
private Cipher encryptCipher;
// Data from client packets
private String loginUsername;
@ -63,13 +68,95 @@ public class NettyPlayerConnection extends PlayerConnection {
private UUID bungeeUuid;
private PlayerSkin bungeeSkin;
private final Object tickBufferLock = new Object();
private volatile ByteBuf tickBuffer = BufUtils.direct();
private final BinaryBuffer tickBuffer = BinaryBuffer.ofSize(Server.SOCKET_BUFFER_SIZE);
private volatile BinaryBuffer cacheBuffer;
public NettyPlayerConnection(@NotNull SocketChannel channel) {
public PlayerSocketConnection(@NotNull Worker worker, @NotNull SocketChannel channel, SocketAddress remoteAddress) {
super();
this.worker = worker;
this.channel = channel;
this.remoteAddress = channel.remoteAddress();
this.remoteAddress = remoteAddress;
}
public void processPackets(Worker.Context workerContext, PacketProcessor packetProcessor) {
final var readBuffer = workerContext.readBuffer;
// Decrypt data
if (encrypted) {
final Cipher cipher = decryptCipher;
final int remainingBytes = readBuffer.readableBytes();
final byte[] bytes = readBuffer.readRemainingBytes();
byte[] output = new byte[cipher.getOutputSize(remainingBytes)];
try {
cipher.update(bytes, 0, remainingBytes, output, 0);
} catch (ShortBufferException e) {
MinecraftServer.getExceptionManager().handleException(e);
return;
}
readBuffer.clear();
readBuffer.writeBytes(output);
}
final int limit = readBuffer.writerOffset();
// Read all packets
while (readBuffer.readableBytes() > 0) {
final var beginMark = readBuffer.mark();
try {
// Ensure that the buffer contains the full packet (or wait for next socket read)
final int packetLength = readBuffer.readVarInt();
final int packetEnd = readBuffer.readerOffset() + packetLength;
if (packetEnd > readBuffer.writerOffset()) {
// Integrity fail
throw new BufferUnderflowException();
}
// Read packet https://wiki.vg/Protocol#Packet_format
BinaryBuffer content;
if (!compressed) {
// Compression disabled, payload is following
content = readBuffer;
} else {
final int dataLength = readBuffer.readVarInt();
if (dataLength == 0) {
// Data is too small to be compressed, payload is following
content = readBuffer;
} else {
// Decompress to content buffer
content = workerContext.contentBuffer;
final var contentStartMark = content.mark();
try {
final var inflater = workerContext.inflater;
inflater.setInput(readBuffer.asByteBuffer(readBuffer.readerOffset(), packetEnd));
inflater.inflate(content.asByteBuffer(0, content.capacity()));
inflater.reset();
} catch (DataFormatException e) {
e.printStackTrace();
}
content.reset(contentStartMark);
}
}
// Process packet
final int packetId = content.readVarInt();
try {
final ByteBuffer payload = content.asByteBuffer(content.readerOffset(), packetEnd);
packetProcessor.process(this, packetId, payload);
} catch (Exception e) {
// Error while reading the packet
MinecraftServer.getExceptionManager().handleException(e);
break;
}
// Position buffer to read the next packet
readBuffer.reset(packetEnd, limit);
} catch (BufferUnderflowException e) {
readBuffer.reset(beginMark);
this.cacheBuffer = BinaryBuffer.copy(readBuffer);
break;
}
}
}
public void consumeCache(BinaryBuffer buffer) {
if (cacheBuffer != null) {
buffer.write(cacheBuffer);
this.cacheBuffer = null;
}
}
/**
@ -80,11 +167,9 @@ public class NettyPlayerConnection extends PlayerConnection {
*/
public void setEncryptionKey(@NotNull SecretKey secretKey) {
Check.stateCondition(encrypted, "Encryption is already enabled!");
this.decryptCipher = MojangCrypt.getCipher(2, secretKey);
this.encryptCipher = MojangCrypt.getCipher(1, secretKey);
this.encrypted = true;
channel.pipeline().addBefore(NettyServer.GROUPED_PACKET_HANDLER_NAME, NettyServer.DECRYPT_HANDLER_NAME,
new Decrypter(MojangCrypt.getCipher(2, secretKey)));
channel.pipeline().addBefore(NettyServer.GROUPED_PACKET_HANDLER_NAME, NettyServer.ENCRYPT_HANDLER_NAME,
new Encrypter(MojangCrypt.getCipher(1, secretKey)));
}
/**
@ -96,11 +181,8 @@ public class NettyPlayerConnection extends PlayerConnection {
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;
writeAndFlush(new SetCompressionPacket(threshold));
channel.pipeline().addAfter(NettyServer.FRAMER_HANDLER_NAME, NettyServer.COMPRESSOR_HANDLER_NAME,
new PacketCompressor(threshold));
this.compressed = true;
}
/**
@ -112,21 +194,16 @@ public class NettyPlayerConnection extends PlayerConnection {
*/
@Override
public void sendPacket(@NotNull ServerPacket serverPacket, boolean skipTranslating) {
if (!channel.isActive())
return;
if (!channel.isConnected()) return;
if (shouldSendPacket(serverPacket)) {
if (getPlayer() != null) {
final Player player = getPlayer();
if (player != null) {
// Flush happen during #update()
if (serverPacket instanceof CacheablePacket && MinecraftServer.hasPacketCaching()) {
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
CacheablePacket.writeCache(tickBuffer, serverPacket);
}
} else {
write(serverPacket, skipTranslating);
if ((MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION && !skipTranslating) && serverPacket instanceof ComponentHoldingServerPacket) {
serverPacket = ((ComponentHoldingServerPacket) serverPacket).copyWithOperator(component ->
GlobalTranslator.render(component, Objects.requireNonNullElseGet(player.getLocale(), MinestomAdventure::getDefaultLocale)));
}
write(serverPacket);
} else {
// Player is probably not logged yet
writeAndFlush(serverPacket);
@ -134,100 +211,61 @@ public class NettyPlayerConnection extends PlayerConnection {
}
}
public void write(@NotNull Object message) {
this.write(message, false);
}
public void write(@NotNull Object message, boolean skipTranslating) {
if (message instanceof FramedPacket) {
final FramedPacket framedPacket = (FramedPacket) message;
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
final ByteBuf body = framedPacket.getBody();
tickBuffer.writeBytes(body, body.readerIndex(), body.readableBytes());
public void write(@NotNull ByteBuffer buffer) {
synchronized (tickBuffer) {
buffer.flip();
if (!tickBuffer.canWrite(buffer.limit())) {
// Tick buffer is full, flush before appending
flush();
}
return;
} else if (message instanceof ServerPacket) {
ServerPacket serverPacket = (ServerPacket) message;
if ((MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION && !skipTranslating) && getPlayer() != null && serverPacket instanceof ComponentHoldingServerPacket) {
serverPacket = ((ComponentHoldingServerPacket) serverPacket).copyWithOperator(component ->
GlobalTranslator.render(component, Objects.requireNonNullElseGet(getPlayer().getLocale(), MinestomAdventure::getDefaultLocale)));
}
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
PacketUtils.writeFramedPacket(tickBuffer, serverPacket);
}
return;
} else if (message instanceof ByteBuf) {
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
tickBuffer.writeBytes((ByteBuf) message);
}
return;
}
throw new UnsupportedOperationException("type " + message.getClass() + " is not supported");
}
public void writeAndFlush(@NotNull Object message) {
writeWaitingPackets();
ChannelFuture channelFuture = channel.writeAndFlush(message);
if (MinecraftServer.shouldProcessNettyErrors()) {
channelFuture.addListener(future -> {
if (!future.isSuccess() && channel.isActive()) {
MinecraftServer.getExceptionManager().handleException(future.cause());
}
});
this.tickBuffer.write(buffer);
}
}
public void writeWaitingPackets() {
if (tickBuffer.writerIndex() == 0) {
// Nothing to write
return;
}
public void write(@NotNull FramedPacket framedPacket) {
write(framedPacket.body());
}
// Retrieve safe copy
final ByteBuf copy;
synchronized (tickBufferLock) {
if (tickBuffer.refCnt() == 0)
return;
copy = tickBuffer;
tickBuffer = tickBuffer.alloc().buffer(tickBuffer.writerIndex());
}
public void write(@NotNull ServerPacket packet) {
// TODO write directly to the tick buffer
write(PacketUtils.createFramedPacket(packet, compressed));
}
// Write copied buffer to netty
ChannelFuture channelFuture = channel.write(new FramedPacket(copy));
channelFuture.addListener(future -> copy.release());
// Netty debug
if (MinecraftServer.shouldProcessNettyErrors()) {
channelFuture.addListener(future -> {
if (!future.isSuccess() && channel.isActive()) {
MinecraftServer.getExceptionManager().handleException(future.cause());
}
});
public void writeAndFlush(@NotNull ServerPacket packet) {
synchronized (tickBuffer) {
write(packet);
flush();
}
}
public void flush() {
final int bufferSize = tickBuffer.writerIndex();
if (bufferSize > 0) {
if (channel.isActive()) {
writeWaitingPackets();
channel.flush();
if (!channel.isOpen()) return;
synchronized (tickBuffer) {
if (tickBuffer.readableBytes() == 0) return;
try {
if (encrypted) {
final Cipher cipher = encryptCipher;
// Encrypt data first
final int remainingBytes = tickBuffer.readableBytes();
final byte[] bytes = tickBuffer.readRemainingBytes();
byte[] outTempArray = new byte[cipher.getOutputSize(remainingBytes)];
cipher.update(bytes, 0, remainingBytes, outTempArray);
this.tickBuffer.clear();
this.tickBuffer.writeBytes(outTempArray);
}
this.tickBuffer.writeChannel(channel);
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
} catch (ShortBufferException e) {
e.printStackTrace();
} finally {
this.tickBuffer.clear();
}
}
}
@NotNull
@Override
public SocketAddress getRemoteAddress() {
public @NotNull SocketAddress getRemoteAddress() {
return remoteAddress;
}
@ -238,19 +276,17 @@ public class NettyPlayerConnection extends PlayerConnection {
*
* @param remoteAddress the new connection remote address
*/
@ApiStatus.Internal
public void setRemoteAddress(@NotNull SocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
@Override
public void disconnect() {
refreshOnline(false);
this.channel.close();
this.worker.disconnect(this, channel);
}
@NotNull
public Channel getChannel() {
public @NotNull SocketChannel getChannel() {
return channel;
}
@ -261,8 +297,7 @@ public class NettyPlayerConnection extends PlayerConnection {
*
* @return the username given by the client, unchecked
*/
@Nullable
public String getLoginUsername() {
public @Nullable String getLoginUsername() {
return loginUsername;
}
@ -309,7 +344,6 @@ public class NettyPlayerConnection extends PlayerConnection {
return protocolVersion;
}
/**
* Used in {@link net.minestom.server.network.packet.client.handshake.HandshakePacket} to change the internal fields.
*
@ -323,9 +357,7 @@ public class NettyPlayerConnection extends PlayerConnection {
this.protocolVersion = protocolVersion;
}
@Nullable
public UUID getBungeeUuid() {
public @Nullable UUID getBungeeUuid() {
return bungeeUuid;
}
@ -333,8 +365,7 @@ public class NettyPlayerConnection extends PlayerConnection {
this.bungeeUuid = bungeeUuid;
}
@Nullable
public PlayerSkin getBungeeSkin() {
public @Nullable PlayerSkin getBungeeSkin() {
return bungeeSkin;
}
@ -367,8 +398,7 @@ public class NettyPlayerConnection extends PlayerConnection {
* @param messageId the message id
* @return the channel linked to the message id, null if not found
*/
@Nullable
public String getPluginRequestChannel(int messageId) {
public @Nullable String getPluginRequestChannel(int messageId) {
return pluginRequestMap.get(messageId);
}
@ -381,12 +411,6 @@ public class NettyPlayerConnection extends PlayerConnection {
}
}
public void releaseTickBuffer() {
synchronized (tickBufferLock) {
tickBuffer.release();
}
}
public byte[] getNonce() {
return nonce;
}

View File

@ -0,0 +1,94 @@
package net.minestom.server.network.socket;
import net.minestom.server.network.PacketProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
public final class Server {
public static final Logger LOGGER = LoggerFactory.getLogger(Server.class);
public static final int WORKER_COUNT = Integer.getInteger("minestom.workers",
Runtime.getRuntime().availableProcessors() * 2);
public static final int SOCKET_BUFFER_SIZE = Integer.getInteger("minestom.buffer-size", 262_143);
public static final int MAX_PACKET_SIZE = 2_097_151; // 3 bytes var-int
public static final boolean NO_DELAY = true;
private volatile boolean stop;
private final Selector selector = Selector.open();
private final List<Worker> workers = new ArrayList<>(WORKER_COUNT);
private int index;
private ServerSocketChannel serverSocket;
private String address;
private int port;
public Server(PacketProcessor packetProcessor) throws IOException {
// Create all workers
for (int i = 0; i < WORKER_COUNT; i++) {
Worker worker = new Worker(this, packetProcessor);
this.workers.add(worker);
worker.start();
}
}
public void start(SocketAddress address) throws IOException {
this.serverSocket = ServerSocketChannel.open();
serverSocket.bind(address);
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
serverSocket.socket().setReceiveBufferSize(SOCKET_BUFFER_SIZE);
LOGGER.info("Server starting, wait for connections");
new Thread(() -> {
while (!stop) {
// Busy wait for connections
try {
this.selector.select(key -> {
if (!key.isAcceptable()) return;
try {
// Register socket and forward to thread
Worker worker = findWorker();
final SocketChannel client = serverSocket.accept();
worker.receiveConnection(client);
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}, "Ms-entrypoint").start();
}
public boolean isOpen() {
return !stop;
}
public void stop() {
this.stop = true;
this.selector.wakeup();
this.workers.forEach(worker -> worker.selector.wakeup());
}
public String getAddress() {
return address;
}
public int getPort() {
return port;
}
private Worker findWorker() {
this.index = ++index % WORKER_COUNT;
return workers.get(index);
}
}

View File

@ -0,0 +1,103 @@
package net.minestom.server.network.socket;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.utils.binary.BinaryBuffer;
import org.jetbrains.annotations.ApiStatus;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.Inflater;
@ApiStatus.Internal
public final class Worker extends Thread {
private static final AtomicInteger COUNTER = new AtomicInteger();
final Selector selector = Selector.open();
private final Context context = new Context();
private final Map<SocketChannel, PlayerSocketConnection> connectionMap = new ConcurrentHashMap<>();
private final Server server;
private final PacketProcessor packetProcessor;
public Worker(Server server, PacketProcessor packetProcessor) throws IOException {
super(null, null, "Ms-worker-" + COUNTER.getAndIncrement());
this.server = server;
this.packetProcessor = packetProcessor;
}
@Override
public void run() {
while (server.isOpen()) {
try {
this.selector.select(key -> {
final SocketChannel channel = (SocketChannel) key.channel();
if (!channel.isOpen()) return;
if (!key.isReadable()) return;
var connection = connectionMap.get(channel);
try {
var readBuffer = context.readBuffer;
// Consume last incomplete packet
connection.consumeCache(readBuffer);
// Read & process
readBuffer.readChannel(channel);
connection.processPackets(context, packetProcessor);
} catch (IOException e) {
// TODO print exception? (should ignore disconnection)
connection.disconnect();
} finally {
context.clearBuffers();
}
});
} catch (IOException e) {
MinecraftServer.getExceptionManager().handleException(e);
}
}
}
public void disconnect(PlayerSocketConnection connection, SocketChannel channel) {
try {
channel.close();
this.connectionMap.remove(channel);
MinecraftServer.getConnectionManager().removePlayer(connection);
connection.refreshOnline(false);
Player player = connection.getPlayer();
if (player != null) {
player.remove();
}
} catch (IOException e) {
e.printStackTrace();
}
}
void receiveConnection(SocketChannel channel) throws IOException {
this.connectionMap.put(channel, new PlayerSocketConnection(this, channel, channel.getRemoteAddress()));
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
var socket = channel.socket();
socket.setSendBufferSize(Server.SOCKET_BUFFER_SIZE);
socket.setReceiveBufferSize(Server.SOCKET_BUFFER_SIZE);
socket.setTcpNoDelay(Server.NO_DELAY);
this.selector.wakeup();
}
/**
* Contains objects that we can be shared across all the connection of a {@link Worker worker}.
*/
public static final class Context {
public final BinaryBuffer readBuffer = BinaryBuffer.ofSize(Server.SOCKET_BUFFER_SIZE);
public final BinaryBuffer contentBuffer = BinaryBuffer.ofSize(Server.MAX_PACKET_SIZE);
public final Inflater inflater = new Inflater();
void clearBuffers() {
this.readBuffer.clear();
this.contentBuffer.clear();
}
}
}

View File

@ -75,13 +75,13 @@ public enum ServerListPingType {
* @see OpenToLAN
*/
public static @NotNull String getOpenToLANPing(@NotNull ResponseData data) {
return String.format(LAN_PING_FORMAT, SECTION.serialize(data.getDescription()), MinecraftServer.getNettyServer().getPort());
return String.format(LAN_PING_FORMAT, SECTION.serialize(data.getDescription()), MinecraftServer.getServer().getPort());
}
/**
* Creates a legacy ping response for client versions below the Netty rewrite (1.6-).
*
* @param data the response data
* @param data the response data
* @param supportsVersions if the client supports recieving the versions of the server
* @return the response
*/
@ -99,7 +99,7 @@ public enum ServerListPingType {
/**
* Creates a modern ping response for client versions above the Netty rewrite (1.7+).
*
* @param data the response data
* @param data the response data
* @param supportsFullRgb if the client supports full RGB
* @return the response
*/

View File

@ -1,13 +0,0 @@
package net.minestom.server.utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
public class BufUtils {
private static final PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
public static ByteBuf direct() {
return alloc.ioBuffer();
}
}

View File

@ -1,6 +1,5 @@
package net.minestom.server.utils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience;
import net.minestom.server.MinecraftServer;
@ -8,28 +7,29 @@ import net.minestom.server.adventure.MinestomAdventure;
import net.minestom.server.adventure.audience.PacketGroupingAudience;
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.FramedPacket;
import net.minestom.server.network.packet.server.ComponentHoldingServerPacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.network.socket.Server;
import net.minestom.server.utils.binary.BinaryWriter;
import net.minestom.server.utils.callback.validator.PlayerValidator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.zip.Deflater;
/**
* Utils class for packets. Including writing a {@link ServerPacket} into a {@link ByteBuf}
* Utils class for packets. Including writing a {@link ServerPacket} into a {@link ByteBuffer}
* for network processing.
*/
public final class PacketUtils {
private static final PacketListenerManager PACKET_LISTENER_MANAGER = MinecraftServer.getPacketListenerManager();
private static final ThreadLocal<Deflater> COMPRESSOR = ThreadLocal.withInitial(Deflater::new);
private static final ThreadLocal<ByteBuffer> BUFFER = ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(Server.SOCKET_BUFFER_SIZE));
private PacketUtils() {
}
@ -77,50 +77,37 @@ public final class PacketUtils {
* @param playerValidator optional callback to check if a specify player of {@code players} should receive the packet
*/
public static void sendGroupedPacket(@NotNull Collection<Player> players, @NotNull ServerPacket packet,
@Nullable PlayerValidator playerValidator) {
@NotNull PlayerValidator playerValidator) {
if (players.isEmpty())
return;
// work out if the packet needs to be sent individually due to server-side translating
boolean needsTranslating = false;
if (MinestomAdventure.AUTOMATIC_COMPONENT_TRANSLATION && packet instanceof ComponentHoldingServerPacket) {
needsTranslating = ComponentUtils.areAnyTranslatable(((ComponentHoldingServerPacket) packet).components());
}
if (MinecraftServer.hasGroupedPacket() && !needsTranslating) {
// Send grouped packet...
final boolean success = PACKET_LISTENER_MANAGER.processServerPacket(packet, players);
if (success) {
final ByteBuf finalBuffer = createFramedPacket(packet);
final FramedPacket framedPacket = new FramedPacket(finalBuffer);
// Send packet to all players
for (Player player : players) {
if (!player.isOnline())
continue;
// Verify if the player should receive the packet
if (playerValidator != null && !playerValidator.isValid(player))
continue;
final PlayerConnection playerConnection = player.getPlayerConnection();
if (playerConnection instanceof NettyPlayerConnection) {
final NettyPlayerConnection nettyPlayerConnection = (NettyPlayerConnection) playerConnection;
nettyPlayerConnection.write(framedPacket, true);
} else {
playerConnection.sendPacket(packet);
}
if (!PACKET_LISTENER_MANAGER.processServerPacket(packet, players))
return;
final ByteBuffer finalBuffer = createFramedPacket(packet);
final FramedPacket framedPacket = new FramedPacket(packet.getId(), finalBuffer);
// Send packet to all players
for (Player player : players) {
if (!player.isOnline() || !playerValidator.isValid(player))
continue;
final PlayerConnection connection = player.getPlayerConnection();
if (connection instanceof PlayerSocketConnection) {
((PlayerSocketConnection) connection).write(framedPacket);
} else {
connection.sendPacket(packet);
}
finalBuffer.release(); // Release last reference
}
} else {
// Write the same packet for each individual players
for (Player player : players) {
// Verify if the player should receive the packet
if (playerValidator != null && !playerValidator.isValid(player))
if (!player.isOnline() || !playerValidator.isValid(player))
continue;
final PlayerConnection playerConnection = player.getPlayerConnection();
playerConnection.sendPacket(packet, false);
player.getPlayerConnection().sendPacket(packet, false);
}
}
}
@ -132,136 +119,78 @@ public final class PacketUtils {
* @see #sendGroupedPacket(Collection, ServerPacket, PlayerValidator)
*/
public static void sendGroupedPacket(@NotNull Collection<Player> players, @NotNull ServerPacket packet) {
sendGroupedPacket(players, packet, null);
sendGroupedPacket(players, packet, player -> true);
}
/**
* Writes a {@link ServerPacket} into a {@link ByteBuf}.
*
* @param buf the recipient of {@code packet}
* @param packet the packet to write into {@code buf}
*/
public static void writePacket(@NotNull ByteBuf buf, @NotNull ServerPacket packet) {
Utils.writeVarInt(buf, packet.getId());
writePacketPayload(buf, packet);
public static void writeFramedPacket(@NotNull ByteBuffer buffer,
@NotNull ServerPacket packet,
boolean compression) {
if (!compression) {
// Length + payload
final int lengthIndex = Utils.writeEmptyVarIntHeader(buffer);
Utils.writeVarInt(buffer, packet.getId());
packet.write(new BinaryWriter(buffer));
final int finalSize = buffer.position() - (lengthIndex + 3);
Utils.writeVarIntHeader(buffer, lengthIndex, finalSize);
return;
}
// Compressed format
final int compressedIndex = Utils.writeEmptyVarIntHeader(buffer);
final int uncompressedIndex = Utils.writeEmptyVarIntHeader(buffer);
final int contentStart = buffer.position();
Utils.writeVarInt(buffer, packet.getId());
packet.write(new BinaryWriter(buffer));
final int packetSize = buffer.position() - contentStart;
if (packetSize >= MinecraftServer.getCompressionThreshold()) {
// Packet large enough, compress
final int limitCache = buffer.limit();
buffer.position(contentStart).limit(contentStart + packetSize);
var uncompressedCopy = ByteBuffer.allocate(packetSize).put(buffer).flip();
buffer.position(contentStart).limit(limitCache);
var deflater = COMPRESSOR.get();
deflater.setInput(uncompressedCopy);
deflater.finish();
deflater.deflate(buffer);
deflater.reset();
Utils.writeVarIntHeader(buffer, compressedIndex, (buffer.position() - contentStart) + 3);
Utils.writeVarIntHeader(buffer, uncompressedIndex, packetSize);
} else {
Utils.writeVarIntHeader(buffer, compressedIndex, packetSize + 3);
Utils.writeVarIntHeader(buffer, uncompressedIndex, 0);
}
}
/**
* Writes a packet payload.
*
* @param packet the packet to write
*/
private static void writePacketPayload(@NotNull ByteBuf buffer, @NotNull ServerPacket packet) {
BinaryWriter writer = new BinaryWriter(buffer);
public static ByteBuffer createFramedPacket(@NotNull ByteBuffer initial, @NotNull ServerPacket packet,
boolean compression) {
var buffer = initial;
try {
packet.write(writer);
} catch (Exception e) {
MinecraftServer.getExceptionManager().handleException(e);
writeFramedPacket(buffer, packet, compression);
} catch (BufferOverflowException e) {
// In the unlikely case where the packet is bigger than the default buffer size,
// increase to the highest authorized buffer size using heap (for cheap allocation)
buffer = ByteBuffer.allocate(Server.MAX_PACKET_SIZE);
writeFramedPacket(buffer, packet, compression);
}
return buffer;
}
/**
* 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.writeVarInt(frameTarget, packetSize);
frameTarget.writeBytes(packetBuffer, packetBuffer.readerIndex(), packetSize);
public static ByteBuffer createFramedPacket(@NotNull ByteBuffer initial, @NotNull ServerPacket packet) {
return createFramedPacket(initial, packet, MinecraftServer.getCompressionThreshold() > 0);
}
/**
* 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 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 ByteBuf packetBuffer, @NotNull ByteBuf compressionTarget) {
final int packetLength = packetBuffer.readableBytes();
final boolean compression = packetLength > MinecraftServer.getCompressionThreshold();
Utils.writeVarInt(compressionTarget, compression ? packetLength : 0);
if (compression) {
compress(deflater, packetBuffer, compressionTarget);
} else {
compressionTarget.writeBytes(packetBuffer);
}
public static ByteBuffer createFramedPacket(@NotNull ServerPacket packet) {
return createFramedPacket(BUFFER.get().clear(), packet);
}
private static void compress(@NotNull Deflater deflater, @NotNull ByteBuf uncompressed, @NotNull ByteBuf compressed) {
deflater.setInput(uncompressed.nioBuffer());
deflater.finish();
while (!deflater.finished()) {
ByteBuffer nioBuffer = compressed.nioBuffer(compressed.writerIndex(), compressed.writableBytes());
compressed.writerIndex(deflater.deflate(nioBuffer) + compressed.writerIndex());
if (compressed.writableBytes() == 0) {
compressed.ensureWritable(8192);
}
}
deflater.reset();
public static ByteBuffer createFramedPacket(@NotNull ServerPacket packet, boolean compression) {
return createFramedPacket(BUFFER.get().clear(), packet, compression);
}
public static void writeFramedPacket(@NotNull ByteBuf buffer,
@NotNull ServerPacket serverPacket) {
final int compressionThreshold = MinecraftServer.getCompressionThreshold();
// Index of the var-int containing the complete packet length
final int packetLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
final int startIndex = buffer.writerIndex(); // Index where the content starts (after length)
if (compressionThreshold > 0) {
// Index of the uncompressed payload length
final int dataLengthIndex = Utils.writeEmpty3BytesVarInt(buffer);
// Write packet
final int contentIndex = buffer.writerIndex();
writePacket(buffer, serverPacket);
final int packetSize = buffer.writerIndex() - contentIndex;
final int uncompressedLength = packetSize >= compressionThreshold ? packetSize : 0;
Utils.write3BytesVarInt(buffer, dataLengthIndex, uncompressedLength);
if (uncompressedLength > 0) {
// Packet large enough, compress
ByteBuf uncompressedCopy = buffer.copy(contentIndex, packetSize);
buffer.writerIndex(contentIndex);
compress(COMPRESSOR.get(), uncompressedCopy, buffer);
uncompressedCopy.release();
}
} else {
// No compression, write packet id + payload
writePacket(buffer, serverPacket);
}
// Total length
final int totalPacketLength = buffer.writerIndex() - startIndex;
Utils.write3BytesVarInt(buffer, packetLengthIndex, totalPacketLength);
}
/**
* Creates a "framed packet" (packet which can be send and understood by a Minecraft client)
* from a server packet, directly into an output buffer.
* <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.
*/
public static @NotNull ByteBuf createFramedPacket(@NotNull ServerPacket serverPacket) {
ByteBuf packetBuf = BufUtils.direct();
writeFramedPacket(packetBuf, serverPacket);
return packetBuf;
public static ByteBuffer allocateTrimmedPacket(@NotNull ServerPacket packet) {
final var temp = PacketUtils.createFramedPacket(packet);
return ByteBuffer.allocateDirect(temp.position()).put(temp.flip());
}
}

View File

@ -1,13 +1,14 @@
package net.minestom.server.utils;
import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.shorts.Short2ShortLinkedOpenHashMap;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.nio.ByteBuffer;
import java.util.UUID;
@ApiStatus.Internal
public final class Utils {
private Utils() {
@ -21,43 +22,46 @@ public final class Utils {
? 4 : 5;
}
public static void writeVarInt(@NotNull ByteBuf buf, int value) {
// Took from velocity
public static void writeVarInt(ByteBuffer buf, int value) {
if ((value & (0xFFFFFFFF << 7)) == 0) {
buf.writeByte(value);
buf.put((byte) value);
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
buf.writeShort(w);
buf.putShort((short) ((value & 0x7F | 0x80) << 8 | (value >>> 7)));
} else if ((value & (0xFFFFFFFF << 21)) == 0) {
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buf.writeMedium(w);
buf.put((byte) (value & 0x7F | 0x80));
buf.put((byte) ((value >>> 7) & 0x7F | 0x80));
buf.put((byte) (value >>> 14));
} else if ((value & (0xFFFFFFFF << 28)) == 0) {
buf.putInt((value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16)
| ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21));
} else {
int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80);
buf.writeInt(w);
buf.writeByte(value >>> 28);
buf.putInt((value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80));
buf.put((byte) (value >>> 28));
}
}
public static void write3BytesVarInt(@NotNull ByteBuf buffer, int startIndex, int value) {
final int indexCache = buffer.writerIndex();
buffer.writerIndex(startIndex);
final int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buffer.writeMedium(w);
buffer.writerIndex(indexCache);
public static void writeVarIntHeader(@NotNull ByteBuffer buffer, int startIndex, int value) {
final int indexCache = buffer.position();
buffer.position(startIndex);
buffer.put((byte) (value & 0x7F | 0x80));
buffer.put((byte) ((value >>> 7) & 0x7F | 0x80));
buffer.put((byte) (value >>> 14));
buffer.position(indexCache);
}
public static int writeEmpty3BytesVarInt(@NotNull ByteBuf buffer) {
final int index = buffer.writerIndex();
buffer.writeMedium(0);
public static int writeEmptyVarIntHeader(@NotNull ByteBuffer buffer) {
final int index = buffer.position();
buffer.putShort((short) 0);
buffer.put((byte) 0);
return index;
}
public static int readVarInt(ByteBuf buf) {
public static int readVarInt(ByteBuffer buf) {
int i = 0;
final int maxRead = Math.min(5, buf.readableBytes());
final int maxRead = Math.min(5, buf.remaining());
for (int j = 0; j < maxRead; j++) {
final int k = buf.readByte();
final int k = buf.get();
i |= (k & 0x7F) << j * 7;
if ((k & 0x80) != 128) {
return i;
@ -66,12 +70,12 @@ public final class Utils {
throw new RuntimeException("VarInt is too big");
}
public static long readVarLong(@NotNull ByteBuf buffer) {
public static long readVarLong(@NotNull ByteBuffer buffer) {
int numRead = 0;
long result = 0;
byte read;
do {
read = buffer.readByte();
read = buffer.get();
long value = (read & 0b01111111);
result |= (value << (7 * numRead));
@ -84,14 +88,14 @@ public final class Utils {
return result;
}
public static void writeVarLong(BinaryWriter writer, long value) {
public static void writeVarLong(ByteBuffer buffer, long value) {
do {
byte temp = (byte) (value & 0b01111111);
value >>>= 7;
if (value != 0) {
temp |= 0b10000000;
}
writer.writeByte(temp);
buffer.put(temp);
} while (value != 0);
}
@ -117,13 +121,12 @@ public final class Utils {
return new UUID(uuidMost, uuidLeast);
}
public static void writePaletteBlocks(ByteBuf buffer, Palette palette) {
public static void writePaletteBlocks(ByteBuffer buffer, Palette palette) {
final short blockCount = palette.getBlockCount();
final int bitsPerEntry = palette.getBitsPerEntry();
buffer.writeShort(blockCount);
buffer.writeByte((byte) bitsPerEntry);
buffer.putShort(blockCount);
buffer.put((byte) bitsPerEntry);
// Palette
if (bitsPerEntry < 9) {
@ -138,7 +141,7 @@ public final class Utils {
final long[] blocks = palette.getBlocks();
writeVarInt(buffer, blocks.length);
for (long datum : blocks) {
buffer.writeLong(datum);
buffer.putLong(datum);
}
}

View File

@ -0,0 +1,177 @@
package net.minestom.server.utils.binary;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jglrxavpok.hephaistos.nbt.NBTReader;
import org.jglrxavpok.hephaistos.nbt.NBTWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
/**
* Manages off-heap memory.
* Not thread-safe.
*/
@ApiStatus.Internal
public final class BinaryBuffer {
private ByteBuffer nioBuffer; // To become a `MemorySegment` once released
private NBTReader nbtReader;
private NBTWriter nbtWriter;
private final int capacity;
private int readerOffset, writerOffset;
private BinaryBuffer(ByteBuffer buffer) {
this.nioBuffer = buffer;
this.capacity = buffer.capacity();
}
@ApiStatus.Internal
public static BinaryBuffer ofSize(int size) {
return new BinaryBuffer(ByteBuffer.allocateDirect(size));
}
public static BinaryBuffer copy(BinaryBuffer buffer) {
final int size = buffer.readableBytes();
final var temp = ByteBuffer.allocateDirect(size)
.put(buffer.asByteBuffer(0, size));
return new BinaryBuffer(temp);
}
public void write(ByteBuffer buffer) {
final int size = buffer.remaining();
// TODO jdk 13 put with index
this.nioBuffer.position(writerOffset).put(buffer);
this.writerOffset += size;
}
public void write(BinaryBuffer buffer) {
write(buffer.asByteBuffer(buffer.readerOffset, buffer.writerOffset));
}
public int readVarInt() {
int value = 0;
for (int i = 0; i < 5; i++) {
final int offset = readerOffset + i;
final byte k = nioBuffer.get(offset);
value |= (k & 0x7F) << i * 7;
if ((k & 0x80) != 128) {
this.readerOffset = offset + 1;
return value;
}
}
throw new RuntimeException("VarInt is too big");
}
public @NotNull Marker mark() {
return new Marker(readerOffset, writerOffset);
}
public void reset(int readerOffset, int writerOffset) {
this.readerOffset = readerOffset;
this.writerOffset = writerOffset;
}
public void reset(@NotNull Marker marker) {
reset(marker.readerOffset(), marker.writerOffset());
}
public boolean canWrite(int size) {
return writerOffset + size <= capacity;
}
public int capacity() {
return capacity;
}
public int readerOffset() {
return readerOffset;
}
public int writerOffset() {
return writerOffset;
}
public int readableBytes() {
return writerOffset - readerOffset;
}
public void writeBytes(byte[] bytes) {
this.nioBuffer.position(writerOffset).put(bytes);
this.writerOffset += bytes.length;
}
public byte[] readBytes(int length) {
byte[] bytes = new byte[length];
this.nioBuffer.position(readerOffset).get(bytes, 0, length);
this.readerOffset += length;
return bytes;
}
public byte[] readRemainingBytes() {
return readBytes(readableBytes());
}
public void clear() {
this.readerOffset = 0;
this.writerOffset = 0;
}
public ByteBuffer asByteBuffer(int reader, int writer) {
return nioBuffer.position(reader).slice().limit(writer);
}
public void writeChannel(WritableByteChannel channel) throws IOException {
final int count = channel.write(asByteBuffer(readerOffset, writerOffset));
if (count == -1) {
// EOS
throw new IOException("Disconnected");
}
this.readerOffset += count;
}
public void readChannel(ReadableByteChannel channel) throws IOException {
final int count = channel.read(asByteBuffer(readerOffset, capacity));
if (count == -1) {
// EOS
throw new IOException("Disconnected");
}
this.writerOffset += count;
}
@Override
public String toString() {
return "BinaryBuffer{" +
"readerOffset=" + readerOffset +
", writerOffset=" + writerOffset +
", capacity=" + capacity +
'}';
}
public static final class Marker {
private final int readerOffset, writerOffset;
private Marker(int readerOffset, int writerOffset) {
this.readerOffset = readerOffset;
this.writerOffset = writerOffset;
}
public int readerOffset() {
return readerOffset;
}
public int writerOffset() {
return writerOffset;
}
@Override
public String toString() {
return "Marker{" +
"readerOffset=" + readerOffset +
", writerOffset=" + writerOffset +
'}';
}
}
}

View File

@ -1,7 +1,5 @@
package net.minestom.server.utils.binary;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minestom.server.coordinate.Point;
@ -17,6 +15,7 @@ import org.jglrxavpok.hephaistos.nbt.NBTReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Supplier;
@ -27,16 +26,15 @@ import java.util.function.Supplier;
* WARNING: not thread-safe.
*/
public class BinaryReader extends InputStream {
private final ByteBuf buffer;
private final ByteBuffer buffer;
private final NBTReader nbtReader = new NBTReader(this, false);
public BinaryReader(@NotNull ByteBuf buffer) {
public BinaryReader(@NotNull ByteBuffer buffer) {
this.buffer = buffer;
}
public BinaryReader(byte[] bytes) {
this(Unpooled.wrappedBuffer(bytes));
this(ByteBuffer.wrap(bytes));
}
public int readVarInt() {
@ -48,49 +46,49 @@ public class BinaryReader extends InputStream {
}
public boolean readBoolean() {
return buffer.readBoolean();
return buffer.get() == 1;
}
public byte readByte() {
return buffer.readByte();
return buffer.get();
}
public short readShort() {
return buffer.readShort();
return buffer.getShort();
}
public char readChar() {
return buffer.readChar();
return buffer.getChar();
}
public int readUnsignedShort() {
return buffer.readUnsignedShort();
return buffer.getShort() & 0xFFFF;
}
/**
* Same as readInt
*/
public int readInteger() {
return buffer.readInt();
return buffer.getInt();
}
/**
* Same as readInteger, created for parity with BinaryWriter
*/
public int readInt() {
return buffer.readInt();
return buffer.getInt();
}
public long readLong() {
return buffer.readLong();
return buffer.getLong();
}
public float readFloat() {
return buffer.readFloat();
return buffer.getFloat();
}
public double readDouble() {
return buffer.readDouble();
return buffer.getDouble();
}
/**
@ -105,12 +103,9 @@ public class BinaryReader extends InputStream {
*/
public String readSizedString(int maxLength) {
final int length = readVarInt();
Check.stateCondition(!buffer.isReadable(length),
"Trying to read a string that is too long (wanted {0}, only have {1})",
length,
buffer.readableBytes());
final String str = buffer.toString(buffer.readerIndex(), length, StandardCharsets.UTF_8);
buffer.skipBytes(length);
byte[] bytes = new byte[length];
buffer.get(bytes);
final String str = new String(bytes, StandardCharsets.UTF_8);
Check.stateCondition(str.length() > maxLength,
"String length ({0}) was higher than the max length of {1}", length, maxLength);
return str;
@ -121,10 +116,8 @@ public class BinaryReader extends InputStream {
}
public byte[] readBytes(int length) {
ByteBuf buf = buffer.readBytes(length);
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
buf.release();
byte[] bytes = new byte[length];
buffer.get(bytes);
return bytes;
}
@ -164,14 +157,11 @@ public class BinaryReader extends InputStream {
}
public Point readBlockPosition() {
final long value = buffer.readLong();
return SerializerUtils.longToBlockPosition(value);
return SerializerUtils.longToBlockPosition(buffer.getLong());
}
public UUID readUuid() {
final long most = readLong();
final long least = readLong();
return new UUID(most, least);
return new UUID(readLong(), readLong());
}
/**
@ -225,7 +215,7 @@ public class BinaryReader extends InputStream {
return (T[]) result;
}
public ByteBuf getBuffer() {
public ByteBuffer getBuffer() {
return buffer;
}
@ -236,7 +226,7 @@ public class BinaryReader extends InputStream {
@Override
public int available() {
return buffer.readableBytes();
return buffer.remaining();
}
public NBT readTag() throws IOException, NBTException {
@ -251,11 +241,12 @@ public class BinaryReader extends InputStream {
* @param extractor the extraction code, simply call the reader's read* methods here.
*/
public byte[] extractBytes(Runnable extractor) {
int startingPosition = getBuffer().readerIndex();
int startingPosition = buffer.position();
extractor.run();
int endingPosition = getBuffer().readerIndex();
int endingPosition = getBuffer().position();
byte[] output = new byte[endingPosition - startingPosition];
getBuffer().getBytes(startingPosition, output);
buffer.get(output, 0, output.length);
//buffer.get(startingPosition, output);
return output;
}
}

View File

@ -1,8 +1,5 @@
package net.minestom.server.utils.binary;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minestom.server.MinecraftServer;
@ -16,8 +13,11 @@ import org.jglrxavpok.hephaistos.nbt.NBTWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Consumer;
@ -26,8 +26,7 @@ import java.util.function.Consumer;
* WARNING: not thread-safe.
*/
public class BinaryWriter extends OutputStream {
private ByteBuf buffer;
private ByteBuffer buffer;
private NBTWriter nbtWriter; // Lazily initialized
/**
@ -36,7 +35,7 @@ public class BinaryWriter extends OutputStream {
* @param initialCapacity the initial capacity of the binary writer
*/
public BinaryWriter(int initialCapacity) {
this.buffer = Unpooled.buffer(initialCapacity);
this.buffer = ByteBuffer.allocate(initialCapacity);
}
/**
@ -44,24 +43,15 @@ public class BinaryWriter extends OutputStream {
*
* @param buffer the writer buffer
*/
public BinaryWriter(@NotNull ByteBuf buffer) {
public BinaryWriter(@NotNull ByteBuffer buffer) {
this.buffer = buffer;
}
/**
* Creates a {@link BinaryWriter} from multiple buffers.
*
* @param buffers the buffers making this
*/
public BinaryWriter(@NotNull ByteBuf... buffers) {
this.buffer = Unpooled.wrappedBuffer(buffers);
}
/**
* Creates a {@link BinaryWriter} with a "reasonably small initial capacity".
*/
public BinaryWriter() {
this.buffer = Unpooled.buffer();
this(500); // TODO prevent OOB
}
/**
@ -79,7 +69,7 @@ public class BinaryWriter extends OutputStream {
* @param b the boolean to write
*/
public void writeBoolean(boolean b) {
buffer.writeBoolean(b);
buffer.put((byte) (b ? 1 : 0));
}
/**
@ -88,7 +78,7 @@ public class BinaryWriter extends OutputStream {
* @param b the byte to write
*/
public void writeByte(byte b) {
buffer.writeByte(b);
buffer.put(b);
}
/**
@ -97,7 +87,7 @@ public class BinaryWriter extends OutputStream {
* @param c the char to write
*/
public void writeChar(char c) {
buffer.writeChar(c);
buffer.putChar(c);
}
/**
@ -106,7 +96,7 @@ public class BinaryWriter extends OutputStream {
* @param s the short to write
*/
public void writeShort(short s) {
buffer.writeShort(s);
buffer.putShort(s);
}
/**
@ -115,7 +105,7 @@ public class BinaryWriter extends OutputStream {
* @param i the int to write
*/
public void writeInt(int i) {
buffer.writeInt(i);
buffer.putInt(i);
}
/**
@ -124,7 +114,7 @@ public class BinaryWriter extends OutputStream {
* @param l the long to write
*/
public void writeLong(long l) {
buffer.writeLong(l);
buffer.putLong(l);
}
/**
@ -133,7 +123,7 @@ public class BinaryWriter extends OutputStream {
* @param f the float to write
*/
public void writeFloat(float f) {
buffer.writeFloat(f);
buffer.putFloat(f);
}
/**
@ -142,7 +132,7 @@ public class BinaryWriter extends OutputStream {
* @param d the double to write
*/
public void writeDouble(double d) {
buffer.writeDouble(d);
buffer.putDouble(d);
}
/**
@ -160,7 +150,7 @@ public class BinaryWriter extends OutputStream {
* @param l the long to write
*/
public void writeVarLong(long l) {
Utils.writeVarLong(this, l);
Utils.writeVarLong(buffer, l);
}
/**
@ -171,9 +161,9 @@ public class BinaryWriter extends OutputStream {
* @param string the string to write
*/
public void writeSizedString(@NotNull String string) {
final int utf8Bytes = ByteBufUtil.utf8Bytes(string);
writeVarInt(utf8Bytes);
buffer.writeCharSequence(string, StandardCharsets.UTF_8);
final var bytes = string.getBytes(StandardCharsets.UTF_8);
writeVarInt(bytes.length);
writeBytes(bytes);
}
/**
@ -184,7 +174,8 @@ public class BinaryWriter extends OutputStream {
* @param charset the charset to encode in
*/
public void writeNullTerminatedString(@NotNull String string, @NotNull Charset charset) {
buffer.writeCharSequence(string + '\0', charset);
final var bytes = (string + '\0').getBytes(charset);
writeBytes(bytes);
}
/**
@ -223,8 +214,8 @@ public class BinaryWriter extends OutputStream {
*
* @param bytes the byte array to write
*/
public void writeBytes(@NotNull byte[] bytes) {
buffer.writeBytes(bytes);
public void writeBytes(byte @NotNull [] bytes) {
buffer.put(bytes);
}
/**
@ -296,12 +287,12 @@ public class BinaryWriter extends OutputStream {
writeable.write(this);
}
public void write(@NotNull BinaryWriter writer) {
this.buffer.writeBytes(writer.getBuffer());
public void write(@NotNull ByteBuffer buffer) {
this.buffer.put(buffer.flip());
}
public void write(@NotNull ByteBuf buffer) {
this.buffer.writeBytes(buffer);
public void write(@NotNull BinaryWriter writer) {
write(writer.buffer);
}
/**
@ -323,46 +314,52 @@ public class BinaryWriter extends OutputStream {
* @return the byte array containing all the {@link BinaryWriter} data
*/
public byte[] toByteArray() {
byte[] bytes = new byte[buffer.readableBytes()];
final int readerIndex = buffer.readerIndex();
buffer.getBytes(readerIndex, bytes);
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
return bytes;
}
/**
* Adds a {@link BinaryWriter}'s {@link ByteBuf} at the beginning of this writer.
* Adds a {@link BinaryWriter}'s {@link ByteBuffer} at the beginning of this writer.
*
* @param headerWriter the {@link BinaryWriter} to add at the beginning
*/
public void writeAtStart(@NotNull BinaryWriter headerWriter) {
// Get the buffer of the header
final ByteBuf headerBuf = headerWriter.getBuffer();
final var headerBuf = headerWriter.getBuffer();
// Merge both the headerBuf and this buffer
final ByteBuf finalBuffer = Unpooled.wrappedBuffer(headerBuf, buffer);
final var finalBuffer = concat(headerBuf, buffer);
// Change the buffer used by this writer
setBuffer(finalBuffer);
}
/**
* Adds a {@link BinaryWriter}'s {@link ByteBuf} at the end of this writer.
* Adds a {@link BinaryWriter}'s {@link ByteBuffer} at the end of this writer.
*
* @param footerWriter the {@link BinaryWriter} to add at the end
*/
public void writeAtEnd(@NotNull BinaryWriter footerWriter) {
// Get the buffer of the footer
final ByteBuf footerBuf = footerWriter.getBuffer();
final var footerBuf = footerWriter.getBuffer();
// Merge both this buffer and the footerBuf
final ByteBuf finalBuffer = Unpooled.wrappedBuffer(buffer, footerBuf);
final var finalBuffer = concat(buffer, footerBuf);
// Change the buffer used by this writer
setBuffer(finalBuffer);
}
public static ByteBuffer concat(final ByteBuffer... buffers) {
final ByteBuffer combined = ByteBuffer.allocate(Arrays.stream(buffers).mapToInt(Buffer::remaining).sum());
Arrays.stream(buffers).forEach(b -> combined.put(b.duplicate()));
return combined;
}
/**
* Gets the raw buffer used by this binary writer.
*
* @return the raw buffer
*/
public @NotNull ByteBuf getBuffer() {
public @NotNull ByteBuffer getBuffer() {
return buffer;
}
@ -371,7 +368,7 @@ public class BinaryWriter extends OutputStream {
*
* @param buffer the new buffer used by this binary writer
*/
public void setBuffer(ByteBuf buffer) {
public void setBuffer(ByteBuffer buffer) {
this.buffer = buffer;
}
@ -381,7 +378,8 @@ public class BinaryWriter extends OutputStream {
}
public void writeUnsignedShort(int yourShort) {
buffer.writeShort(yourShort & 0xFFFF);
// FIXME unsigned
buffer.putShort((short) (yourShort & 0xFFFF));
}
/**

View File

@ -1,9 +0,0 @@
package net.minestom.server.utils.binary;
public final class BitmaskUtil {
public static byte changeBit(byte value, byte mask, byte replacement, byte shift) {
return (byte) (value & ~mask | (replacement << shift));
}
}

View File

@ -1,91 +0,0 @@
package net.minestom.server.utils.cache;
import io.netty.buffer.ByteBuf;
import net.minestom.server.network.netty.packet.FramedPacket;
import net.minestom.server.network.packet.server.ServerPacket;
import net.minestom.server.utils.PacketUtils;
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 differentiate this packet from the others of the same type.
*/
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 prevent caching
*/
@Nullable UUID getIdentifier();
/**
* Gets the last time this packet changed.
*
* @return the last packet update time in milliseconds
*/
long getTimestamp();
static @Nullable FramedPacket getCache(@NotNull ServerPacket serverPacket) {
if (!(serverPacket instanceof CacheablePacket))
return null;
final CacheablePacket cacheablePacket = (CacheablePacket) serverPacket;
final UUID identifier = cacheablePacket.getIdentifier();
if (identifier == null) {
// This packet explicitly asks to do not retrieve the cache
return null;
} else {
final long timestamp = cacheablePacket.getTimestamp();
// Try to retrieve the cached buffer
TemporaryCache<TimedBuffer> temporaryCache = cacheablePacket.getCache();
TimedBuffer timedBuffer = temporaryCache.retrieve(identifier);
// Update the buffer if non-existent or outdated
final boolean shouldUpdate = timedBuffer == null ||
timestamp > timedBuffer.getTimestamp();
if (shouldUpdate) {
// Buffer freed by guava cache #removalListener
final ByteBuf buffer = PacketUtils.createFramedPacket(serverPacket);
timedBuffer = new TimedBuffer(buffer, timestamp);
temporaryCache.cache(identifier, timedBuffer);
}
return new FramedPacket(timedBuffer.getBuffer());
}
}
static void writeCache(@NotNull ByteBuf buffer, @NotNull ServerPacket serverPacket) {
FramedPacket framedPacket = CacheablePacket.getCache(serverPacket);
if (framedPacket == null) {
PacketUtils.writeFramedPacket(buffer, serverPacket);
return;
}
final ByteBuf body = framedPacket.getBody();
synchronized (body) {
if (framedPacket.getBody().refCnt() != 0) {
buffer.writeBytes(body, body.readerIndex(), body.readableBytes());
} else {
PacketUtils.writeFramedPacket(buffer, serverPacket);
}
}
}
}

View File

@ -1,57 +0,0 @@
package net.minestom.server.utils.cache;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Cache objects with a timeout.
*
* @param <T> the object type to cache
*/
public class TemporaryCache<T> {
private final Cache<UUID, T> cache;
/**
* Creates a new temporary cache.
*
* @param duration the time before considering an object unused
*/
public TemporaryCache(long duration, TimeUnit timeUnit, RemovalListener<UUID, T> removalListener) {
this.cache = Caffeine.newBuilder()
.expireAfterWrite(duration, timeUnit)
.removalListener(removalListener)
.build();
}
/**
* Caches an object.
*
* @param identifier the object identifier
* @param value the object to cache
*/
public void cache(@NotNull UUID identifier, T value) {
this.cache.put(identifier, value);
}
public void invalidate(@NotNull UUID identifier) {
this.cache.invalidate(identifier);
}
/**
* 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.getIfPresent(identifier);
}
}

View File

@ -1,18 +0,0 @@
package net.minestom.server.utils.cache;
import io.netty.buffer.ByteBuf;
import java.util.concurrent.TimeUnit;
public class TemporaryPacketCache extends TemporaryCache<TimedBuffer> {
public TemporaryPacketCache(long duration, TimeUnit timeUnit) {
super(duration, timeUnit, (key, value, cause) -> {
if (value == null)
return;
final ByteBuf buffer = value.getBuffer();
synchronized (buffer) {
buffer.release();
}
});
}
}

View File

@ -1,28 +0,0 @@
package net.minestom.server.utils.cache;
import io.netty.buffer.ByteBuf;
import org.jetbrains.annotations.NotNull;
/**
* Object containing a {@link ByteBuf buffer} and its timestamp.
* Used for packet-caching to use the most recent.
*/
public class TimedBuffer {
private final ByteBuf buffer;
private final long timestamp;
public TimedBuffer(@NotNull ByteBuf buffer, long timestamp) {
this.buffer = buffer;
this.timestamp = timestamp;
}
@NotNull
public ByteBuf getBuffer() {
return buffer;
}
public long getTimestamp() {
return timestamp;
}
}

View File

@ -2,25 +2,23 @@ package net.minestom.server.utils.player;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.network.player.NettyPlayerConnection;
import net.minestom.server.network.player.PlayerSocketConnection;
import net.minestom.server.network.player.PlayerConnection;
public final class PlayerUtils {
private PlayerUtils() {
}
public static boolean isNettyClient(PlayerConnection playerConnection) {
return playerConnection instanceof NettyPlayerConnection;
public static boolean isSocketClient(PlayerConnection playerConnection) {
return playerConnection instanceof PlayerSocketConnection;
}
public static boolean isNettyClient(Player player) {
return isNettyClient(player.getPlayerConnection());
public static boolean isSocketClient(Player player) {
return isSocketClient(player.getPlayerConnection());
}
public static boolean isNettyClient(Entity entity) {
return (entity instanceof Player) && isNettyClient((Player) entity);
public static boolean isSocketClient(Entity entity) {
return (entity instanceof Player) && isSocketClient((Player) entity);
}
}