Archived
0

refactoring: swap modules

This commit is contained in:
2021-06-17 15:09:29 +03:00
parent e7f7b9654e
commit 696d18cf41
87 changed files with 3 additions and 5 deletions

11
protocol/build.gradle Normal file
View File

@@ -0,0 +1,11 @@
apply from: rootDir.toPath().resolve('logic.gradle').toFile()
dependencies {
api project(':utils')
implementation libs.netty.transport
implementation libs.netty.codec
implementation libs.json
testImplementation libs.lang3
}

View File

@@ -0,0 +1,2 @@
# suppress inspection "UnusedProperty" for whole file
module.name=protocol

View File

@@ -0,0 +1,10 @@
package mc.protocol;
import io.netty.util.AttributeKey;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ProtocolAttributes {
public static final AttributeKey<State> STATE = AttributeKey.newInstance("STATE");
}

View File

@@ -0,0 +1,10 @@
package mc.protocol;
import lombok.experimental.UtilityClass;
@UtilityClass
public class ProtocolConstant {
public final String PROTOCOL_NAME = "1.12.2";
public final int PROTOCOL_NUMBER = 340;
}

View File

@@ -0,0 +1,102 @@
package mc.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.packets.KeepAlivePacket;
import mc.protocol.packets.ServerSidePacket;
import mc.protocol.packets.handshaking.client.HandshakePacket;
import mc.protocol.packets.login.client.LoginStartPacket;
import mc.protocol.packets.login.server.DisconnectPacket;
import mc.protocol.packets.login.server.LoginSuccessPacket;
import mc.protocol.packets.play.client.*;
import mc.protocol.packets.play.server.*;
import mc.protocol.packets.status.client.StatusServerRequestPacket;
import mc.protocol.packets.status.server.StatusServerResponse;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.Map;
@RequiredArgsConstructor
public enum State {
HANDSHAKING(0,
// client side
Map.of(0x00, HandshakePacket.class),
// server side
Collections.emptyMap()
),
STATUS(1,
// client side
Map.of(
0x00, StatusServerRequestPacket.class,
0x01, KeepAlivePacket.class
),
// server side
Map.of(
StatusServerResponse.class, 0x00,
KeepAlivePacket.class, 0x01
)
),
LOGIN(2,
// client side
Map.of(0x00, LoginStartPacket.class),
// server side
Map.of(
DisconnectPacket.class, 0x00,
LoginSuccessPacket.class, 0x02
)
),
PLAY(3,
// client side
Map.of(
0x00, TeleportConfirmPacket.class,
0x04, ClientSettingsPacket.class,
0x09, PluginMessagePacket.class,
0x0B, KeepAlivePacket.class,
0x0D, PlayerPositionPacket.class,
0x0E, CPlayerPositionAndLookPacket.class,
0x0F, PlayerLookPacket.class,
0x15, EntityActionPacket.class
),
// server side
Map.of(
KeepAlivePacket.class, 0x1F,
ChunkDataPacket.class, 0x20,
JoinGamePacket.class, 0x23,
PlayerAbilitiesPacket.class,0x2C,
SPlayerPositionAndLookPacket.class, 0x2F,
SpawnPositionPacket.class, 0x46
)
);
public static State getById(int id) {
// а зачем усложнять?
//@formatter:off
if (id == 1) return STATUS;
else if (id == 2) return LOGIN;
else if (id == 3) return PLAY;
else return HANDSHAKING;
//@formatter:on
}
@Getter
private final int id;
private final Map<Integer, Class<? extends ClientSidePacket>> clientSidePackets;
private final Map<Class<? extends ServerSidePacket>, Integer> serverSidePackets;
@Nullable
public Class<? extends ClientSidePacket> getClientSidePacketById(int id) {
return clientSidePackets == null ? null : clientSidePackets.get(id);
}
@Nullable
public Integer getServerSidePacketId(Class<? extends ServerSidePacket> clazz) {
return serverSidePackets == null ? null : serverSidePackets.get(clazz);
}
}

View File

@@ -0,0 +1,192 @@
package mc.protocol.buffer;
import io.netty.buffer.ByteBuf;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.model.text.Text;
import mc.protocol.model.text.TextSerializer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* Компонент чтения и записи данных протокола.
*
* <p>Data types</p>
* <pre>
* | TYPE | SIZE (bytes) | ENCODING | NOTES |
* |----------------|-----------------------|-----------------------------------------------------|--------------------------------------------------------------------------|
* | Boolean | 1 | True или False | True = 0x01; False = 0x00 |
* | Byte | 1 | Число от -128 до 127 | 8-bit число со знаком |
* | Unsigned Byte | 1 | Число от 0 до 255 | 8-bit без знаковое число |
* | Short | 2 | Число от -32768 до 32767 | 16-bit число со знаком |
* | Unsigned Short | 2 | Число от -32768 до 32767 | 16-bit без знаковое число |
* | Int | 4 | Число от -2147483648 и 2147483647 | 32-bit число со знаком |
* | Long | 8 | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число со знаком |
* | Float | 4 | 32-bit число одинарной точности (IEEE 754-2008) | [1] |
* | Double | 8 | 64-bit число одинарной точности (IEEE 754-2008) | [2] |
* | String (n) | >= 1 ; <= (n * 4) + 3 | Последовательность Unicode scalar values | В начале пишется длина строки в VarInt, после чего записываются символы. |
* | | | | Каждый символ может состоять максимум из 4 байт. [3] |
* | | | | Максимальная длина строки - 32767 (3 - это как раз размер VarInt для |
* | | | | этого числа). |
* | VarInt | >= 1 ; <= 5 | Число от -2147483648 и 2147483647 | 32-bit число с плавающей размерностью от 1 до 5 байт |
* | VarLong | >= 1 ; <= 10 | Число от -9223372036854775808 и 9223372036854775807 | 64-bit число с плавающей размерностью от 1 до 10 байт |
* | Text | | JSON | По файту является String (n), который имеет формат JSON |
*
* [1] - <a href="https://en.wikipedia.org/wiki/Single-precision_floating-point_format">Single-precision floating-point format</a>
* [2] - <a href="https://en.wikipedia.org/wiki/Double-precision_floating-point_format">Double-precision floating-point format</a>
* [3] - <a href="http://unicode.org/glossary/#unicode_scalar_value">Unicode Scalar Value</a>
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Data_types&oldid=14345#Definitions">Data types</a>
* @see <a href="https://wiki.vg/index.php?title=Chat&oldid=14272">Chat</a>
*/
@Slf4j
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString
public class NetByteBuf extends ByteBuf {
@Delegate
private final ByteBuf byteBuf;
public void writeUnsignedByte(int value) {
byteBuf.writeByte((byte)(value & 0xFF));
}
public void writeText(Text text) {
writeString(TextSerializer.toStringPlain(text));
}
//region String
public String readString() {
return readString(Short.MAX_VALUE);
}
@SuppressWarnings("java:S131")
public String readString(int maxLength) {
int length = readVarInt();
if (length == 0) {
return "";
} else if (length > maxLength) {
throw new NetIOException("String length exceeds maximum length: " + length + " > " + maxLength);
} else if (length < 0) {
throw new NetIOException("String length less zero!");
}
byte[] bytes = new byte[length * 4];
int readbleBytes = 0;
for (int i = 0; i < length && readableBytes() > 0; i++) {
byte b = readByte();
bytes[readbleBytes++] = b;
switch ((b & 0xFF) >> 4) {
case 0b1100:
case 0b1101:
bytes[readbleBytes++] = readByte();
break;
case 0b1110:
bytes[readbleBytes++] = readByte();
bytes[readbleBytes++] = readByte();
break;
case 0b1111:
bytes[readbleBytes++] = readByte();
bytes[readbleBytes++] = readByte();
bytes[readbleBytes++] = readByte();
break;
}
}
return new String(bytes, 0, readbleBytes, StandardCharsets.UTF_8);
}
public void writeString(String string) {
byte[] buf = string.getBytes(StandardCharsets.UTF_8);
if (buf.length > Short.MAX_VALUE) {
log.warn("String is too long: {} > {}", buf.length, Short.MAX_VALUE);
writeVarInt(Short.MAX_VALUE);
writeBytes(buf, 0, Short.MAX_VALUE);
} else {
writeVarInt(buf.length);
writeBytes(buf);
}
}
//endregion
//region VarInt
public int readVarInt() {
int numRead = 0;
int result = 0;
byte read;
do {
if ((numRead + 1) > 5) {
log.warn("VarInt is too big");
break;
}
read = readByte();
int value = (read & 0b01111111);
result |= (value << (7 * numRead));
numRead++;
} while ((read & 0b10000000) != 0);
return result;
}
public void writeVarInt(int value) {
while ((value & -128) != 0) {
writeByte(value & 127 | 128);
value >>>= 7;
}
writeByte(value);
}
//endregion
//region VarLong
public long readVarLong() {
int numRead = 0;
long result = 0L;
byte read;
do {
if (numRead > 10) {
log.warn("VarLong is too big");
break;
}
read = readByte();
long value = (read & 0b01111111);
result |= (value << (7 * numRead));
numRead++;
} while ((read & 0b10000000) != 0);
return result;
}
public void writeVarLong(long value) {
while ((value & -128L) != 0L) {
writeByte((int) (value & 127L) | 128);
value >>>= 7;
}
writeByte((int) value);
}
//endregion
//region UUID
public UUID readUUID() {
return new UUID(readLong(), readLong());
}
public void writeUUID(UUID uuid) {
writeLong(uuid.getMostSignificantBits());
writeLong(uuid.getLeastSignificantBits());
}
//endregion
}

View File

@@ -0,0 +1,8 @@
package mc.protocol.buffer;
public class NetIOException extends RuntimeException {
public NetIOException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
package mc.protocol.handler;
import io.netty.channel.ChannelHandlerContext;
import mc.protocol.State;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.utils.Table;
@SuppressWarnings({ "rawtypes", "unchecked" })
public class ProtocolHandlersBus {
private final Table<State, Class<? extends ClientSidePacket>, Handler> table = new Table<>();
public <P extends ClientSidePacket> ProtocolHandlersBus addHandler(State state, Class<P> packetClass, Handler<P> handler) {
table.put(state, packetClass, handler);
return this;
}
public <P extends ClientSidePacket> void process(State state, ChannelHandlerContext ctx, P packet) {
Handler handler = table.getColumnAndRow(state, packet.getClass());
if (handler != null) {
handler.handle(ctx, packet);
}
}
@FunctionalInterface
public interface Handler<P extends ClientSidePacket> {
void handle(ChannelHandlerContext ctx, P packet);
}
}

View File

@@ -0,0 +1,39 @@
package mc.protocol.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.ProtocolAttributes;
import mc.protocol.State;
import mc.protocol.packets.ClientSidePacket;
import java.io.IOException;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
public class ProtocolInboundHandler extends SimpleChannelInboundHandler<ClientSidePacket> {
private static final String CLIENT_FORCE_DISCONNECTED_IOEXCEPTION_MESSAGE_RU = "Программа на вашем хост-компьютере разорвала установленное подключение";
private final ProtocolHandlersBus protocolHandlersBus;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ClientSidePacket packet) {
State state = Objects.requireNonNull(ctx.channel().attr(ProtocolAttributes.STATE).get());
protocolHandlersBus.process(state, ctx, packet);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException && cause.getLocalizedMessage().equalsIgnoreCase(CLIENT_FORCE_DISCONNECTED_IOEXCEPTION_MESSAGE_RU)) {
log.warn("Client '{}' force disconnected", ctx.channel().remoteAddress());
if (log.isTraceEnabled()) {
log.trace("{}", cause.getMessage(), cause);
}
} else {
log.error("{}", cause.getMessage(), cause);
}
}
}

View File

@@ -0,0 +1,65 @@
package mc.protocol.handler.codec;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.ProtocolAttributes;
import mc.protocol.State;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.packets.UnknownPacket;
import mc.protocol.pool.PacketObjectPool;
import java.util.List;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
public class ProtocolDecoder extends ByteToMessageDecoder {
private final boolean readUnknownPackets;
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.channel().attr(ProtocolAttributes.STATE).set(State.HANDSHAKING);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
State state = Objects.requireNonNull(ctx.channel().attr(ProtocolAttributes.STATE).get());
NetByteBuf netByteBuf = new NetByteBuf(in);
int packetId = netByteBuf.readVarInt();
Class<? extends ClientSidePacket> packetClass = state.getClientSidePacketById(packetId);
if (packetClass == null) {
log.warn("Unknown packet: State {} ; Id 0x{}", state, packetIdAsHexcode(packetId));
if (readUnknownPackets) {
UnknownPacket unknownPacket = new UnknownPacket();
unknownPacket.setState(state);
unknownPacket.setId(packetId);
unknownPacket.setDataSize(netByteBuf.readableBytes());
unknownPacket.readSelf(netByteBuf);
out.add(unknownPacket);
} else {
netByteBuf.skipBytes(netByteBuf.readableBytes());
}
} else {
ClientSidePacket packet = PacketObjectPool.getInstance().getPool(packetClass).borrowObject();
packet.readSelf(netByteBuf);
if (log.isDebugEnabled()) {
log.debug("IN: {}:{}", state, packet);
}
out.add(packet);
}
}
private static String packetIdAsHexcode(int packetId) {
String hexPacketId = Integer.toHexString(packetId).toUpperCase();
if (hexPacketId.length() == 1) hexPacketId = "0" + hexPacketId;
return hexPacketId;
}
}

View File

@@ -0,0 +1,39 @@
package mc.protocol.handler.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.extern.slf4j.Slf4j;
import mc.protocol.ProtocolAttributes;
import mc.protocol.State;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
import java.util.Objects;
@Slf4j
public class ProtocolEncoder extends MessageToByteEncoder<ServerSidePacket> {
@Override
protected void encode(ChannelHandlerContext ctx, ServerSidePacket packet, ByteBuf out) {
State state = Objects.requireNonNull(ctx.channel().attr(ProtocolAttributes.STATE).get());
Integer packetId = state.getServerSidePacketId(packet.getClass());
if (packetId == null) {
log.error("Unknown send packet: State {} ; Class {}", state, packet.getClass());
return;
}
if (log.isDebugEnabled()) {
log.debug("OUT: {}:{}", state, packet);
}
NetByteBuf buffer = new NetByteBuf(Unpooled.buffer());
buffer.writeVarInt(packetId);
packet.writeSelf(buffer);
NetByteBuf netByteBuf = new NetByteBuf(out);
netByteBuf.writeVarInt(buffer.readableBytes());
netByteBuf.writeBytes(buffer);
}
}

View File

@@ -0,0 +1,40 @@
package mc.protocol.handler.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import mc.protocol.buffer.NetByteBuf;
import java.util.List;
public class ProtocolSplitter extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
NetByteBuf netByteBuf = new NetByteBuf(in);
netByteBuf.markReaderIndex();
do {
byte[] sizePacketRaw = new byte[3];
for (int i = 0; i < 3; ++i) {
sizePacketRaw[i] = netByteBuf.readByte();
if (sizePacketRaw[i] >= 0) {
break;
}
}
int sizePacket = new NetByteBuf(Unpooled.wrappedBuffer(sizePacketRaw)).readVarInt();
if (netByteBuf.readableBytes() >= sizePacket) {
byte[] bytes = new byte[sizePacket];
netByteBuf.readBytes(bytes);
out.add(Unpooled.wrappedBuffer(bytes));
} else {
netByteBuf.resetReaderIndex();
break;
}
} while (netByteBuf.readableBytes() > 0);
}
}

View File

@@ -0,0 +1,18 @@
package mc.protocol.model;
import lombok.Data;
@Data
public class Location {
private double x = 0d;
private double y = 0d;
private double z = 0d;
public Location set(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
}

View File

@@ -0,0 +1,16 @@
package mc.protocol.model;
import lombok.Data;
@Data
public class Look {
private float yaw;
private float pitch;
public Look set(float yaw, float pitch) {
this.yaw = yaw;
this.pitch = pitch;
return this;
}
}

View File

@@ -0,0 +1,50 @@
package mc.protocol.model;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.RequiredArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import mc.protocol.model.text.Text;
import java.util.List;
@Accessors(fluent = true)
@Getter
@Setter
@ToString
public class ServerInfo {
private final Version version = new Version();
private final Players players = new Players();
private Text description;
private String favicon;
@Getter
@Setter
@ToString
public static class Version {
private String name;
private int protocol;
}
@Getter
@Setter
@ToString
public static class Players {
private int max;
private int online;
private List<SamplePlayer> sample;
}
@RequiredArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public static class SamplePlayer {
private final String id;
private final String name;
}
}

View File

@@ -0,0 +1,59 @@
package mc.protocol.model;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import lombok.experimental.UtilityClass;
import mc.protocol.model.text.TextSerializer;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Collector;
import java.util.stream.StreamSupport;
@UtilityClass
public class ServerInfoSerializer {
public String toStringPlain(ServerInfo info) {
return toJsonObject(info).toString();
}
private JsonObject toJsonObject(ServerInfo info) {
JsonObject jsonObject = Json.object()
.add("version", createVersionObj(info))
.add("players", createPlayersObj(info))
.add("description", TextSerializer.toJsonObject(info.description()));
if (info.favicon() != null && !info.favicon().isEmpty()) {
jsonObject.add("favicon", info.favicon());
}
return jsonObject;
}
private JsonObject createVersionObj(ServerInfo info) {
return Json.object()
.add("name", info.version().name())
.add("protocol", info.version().protocol());
}
private JsonObject createPlayersObj(ServerInfo info) {
JsonArray sampleArr = info.players().sample().stream()
.map(samplePlayer -> Json.object()
.add("name", samplePlayer.name())
.add("id", samplePlayer.id()))
.collect(Collector.of(Json::array, JsonArray::add, ServerInfoSerializer::jsonArrayAddAll));
return Json.object()
.add("max", info.players().max())
.add("online", info.players().online())
.add("sample", sampleArr);
}
private static JsonArray jsonArrayAddAll(JsonArray jsonArrayTo, JsonArray jsonArrayFrom) {
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(jsonArrayFrom.iterator(), Spliterator.ORDERED), false)
.forEach(jsonArrayTo::add);
return jsonArrayTo;
}
}

View File

@@ -0,0 +1,151 @@
package mc.protocol.model.text;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.Getter;
import lombok.experimental.Accessors;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
@Accessors(fluent = true)
@AllArgsConstructor
@Data
public class Text {
private TextColor color;
private TextStyle style;
private String content;
private List<Text> children;
public static Text of(String string) {
return new Text(null, null, string, null);
}
public static Text of(TextColor color, String string) {
return new Text(color, null, string, null);
}
public static Text of(TextStyle style, String string) {
return new Text(null, style, string, null);
}
public static Text of(TextColor color, TextStyle style, String string) {
return new Text(color, style, string, null);
}
public static Builder builder() {
return new Builder();
}
@NoArgsConstructor
@ToString
public static class Builder {
@Getter(onMethod = @__(@Nullable))
private StringBuilder contentBuilder;
private TextStyle.Builder styleBuilder;
private TextColor color;
private List<Text> children;
public Builder append(char content) {
if (this.contentBuilder == null) {
this.contentBuilder = new StringBuilder();
}
this.contentBuilder.append(content);
return this;
}
public Builder append(String content) {
if (this.contentBuilder == null) {
this.contentBuilder = new StringBuilder(content);
} else {
this.contentBuilder.append(content);
}
return this;
}
public Builder append(Text text) {
if (children == null) {
children = new ArrayList<>();
}
children.add(text);
return this;
}
public Builder style(TextStyle style) {
//@formatter:off
if (style.bold() != null) bold(style.bold());
if (style.italic() != null) italic(style.italic());
if (style.underline() != null) underline(style.underline());
if (style.strikethrough() != null) strikethrough(style.strikethrough());
if (style.obfuscated() != null) obfuscated(style.obfuscated());
//@formatter:on
return this;
}
public Builder color(TextColor color) {
this.color = color;
return this;
}
public Builder bold(Boolean bold) {
if (this.styleBuilder == null) {
this.styleBuilder = TextStyle.builder();
}
this.styleBuilder.bold(bold);
return this;
}
public Builder italic(Boolean italic) {
if (this.styleBuilder == null) {
this.styleBuilder = TextStyle.builder();
}
this.styleBuilder.italic(italic);
return this;
}
public Builder underline(Boolean underline) {
if (this.styleBuilder == null) {
this.styleBuilder = TextStyle.builder();
}
this.styleBuilder.underline(underline);
return this;
}
public Builder strikethrough(Boolean strikethrough) {
if (this.styleBuilder == null) {
this.styleBuilder = TextStyle.builder();
}
this.styleBuilder.strikethrough(strikethrough);
return this;
}
public Builder obfuscated(Boolean obfuscated) {
if (this.styleBuilder == null) {
this.styleBuilder = TextStyle.builder();
}
this.styleBuilder.obfuscated(obfuscated);
return this;
}
public Text build() {
return new Text(
color,
styleBuilder == null ? null : styleBuilder.build(),
contentBuilder == null ? null : contentBuilder.toString(),
children);
}
}
}

View File

@@ -0,0 +1,30 @@
package mc.protocol.model.text;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum TextColor {
//@formatter:off
BLACK ("black", '0'),
DARK_BLUE ("dark_blue", '1'),
DARK_GREEN ("dark_green", '2'),
DARK_AQUA ("dark_aqua", '3'),
DARK_RED ("dark_red", '4'),
DARK_PUEPLE("dark_purple", '5'),
GOLD ("gold", '6'),
GRAY ("gray", '7'),
DARK_GRAY ("dark_gray", '8'),
BLUE ("blue", '9'),
GREEN ("green", 'a'),
AQUA ("aqua", 'b'),
RED ("red", 'c'),
PURPLE ("light_purple",'d'),
YELLOW ("yellow", 'e'),
WHITE ("white", 'f');
//@formatter:on
private final String name;
private final char code;
}

View File

@@ -0,0 +1,130 @@
package mc.protocol.model.text;
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import lombok.experimental.UtilityClass;
import java.util.Map;
@UtilityClass
public class TextSerializer {
private static final Map<Character, TextStyle> legacyStyleCodes;
private static final Map<Character, TextColor> legacyColorCodes;
public String toStringPlain(Text text) {
return toJsonObject(text).toString();
}
public JsonObject toJsonObject(Text text) {
JsonObject jsonObject = Json.object();
if (text.content() != null) {
jsonObject.add("text", text.content());
}
if (text.color() != null) {
jsonObject.add("color", text.color().getName());
}
if (text.style() != null) {
//@formatter:off
if (text.style().bold() != null) jsonObject.add("bold", text.style().bold());
if (text.style().italic() != null) jsonObject.add("italic", text.style().italic());
if (text.style().underline() != null) jsonObject.add("underline", text.style().underline());
if (text.style().strikethrough() != null) jsonObject.add("strikethrough", text.style().strikethrough());
if (text.style().obfuscated() != null) jsonObject.add("obfuscated", text.style().obfuscated());
//@formatter:on
}
if (text.children() != null && !text.children().isEmpty()) {
JsonArray extra = Json.array();
text.children().forEach(child -> extra.add(toJsonObject(child)));
jsonObject.add("extra", extra);
}
return jsonObject;
}
/**
* Преобразование строки вида "&4красный" в {@link Text}.
*
* @param string тест
* @return Text
*/
@SuppressWarnings({"java:S3776", "java:S2583", "java:S135"})
public Text fromPlain(String string) {
boolean flagSys = false;
Text.Builder rootTextBuilder = Text.builder();
Text.Builder textBuilder = rootTextBuilder;
for (char ch : string.toCharArray()) {
if (!flagSys) {
if ('&' == ch) {
flagSys = true;
} else {
textBuilder.append(ch);
}
continue;
}
if (!legacyStyleCodes.containsKey(ch) && !legacyColorCodes.containsKey(ch) && '&' == ch) {
textBuilder.append('&');
flagSys = false;
continue;
}
//noinspection ConstantConditions
if (textBuilder.contentBuilder() != null && textBuilder.contentBuilder().length() > 0) {
if (textBuilder != rootTextBuilder) {
rootTextBuilder.append(textBuilder.build());
}
textBuilder = Text.builder();
}
if (legacyStyleCodes.containsKey(ch)) {
textBuilder.style(legacyStyleCodes.get(ch));
} else {
textBuilder.color(legacyColorCodes.get(ch));
}
flagSys = false;
}
if (textBuilder != rootTextBuilder) {
rootTextBuilder.append(textBuilder.build());
}
return rootTextBuilder.build();
}
static {
legacyColorCodes = Map.ofEntries(
Map.entry('0', TextColor.BLACK),
Map.entry('1', TextColor.DARK_BLUE),
Map.entry('2', TextColor.DARK_GREEN),
Map.entry('3', TextColor.DARK_AQUA),
Map.entry('4', TextColor.DARK_RED),
Map.entry('5', TextColor.DARK_PUEPLE),
Map.entry('6', TextColor.GOLD),
Map.entry('7', TextColor.GRAY),
Map.entry('8', TextColor.DARK_GRAY),
Map.entry('9', TextColor.BLUE),
Map.entry('a', TextColor.GREEN),
Map.entry('b', TextColor.AQUA),
Map.entry('c', TextColor.RED),
Map.entry('d', TextColor.PURPLE),
Map.entry('e', TextColor.YELLOW),
Map.entry('f', TextColor.WHITE)
);
legacyStyleCodes = Map.of(
'k', TextStyle.OBFUSCATED,
'l', TextStyle.BOLD,
'm', TextStyle.STRIKETHOUGH,
'n', TextStyle.UNDERLINE,
'o', TextStyle.ITALIC
);
}
}

View File

@@ -0,0 +1,82 @@
package mc.protocol.model.text;
import lombok.*;
import lombok.experimental.Accessors;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Accessors(fluent = true)
@Data
@SuppressWarnings("java:S1845")
public class TextStyle {
public static final TextStyle BOLD = new TextStyle(true, null, null, null, null);
public static final TextStyle ITALIC = new TextStyle(null, true, null, null, null);
public static final TextStyle UNDERLINE = new TextStyle(null, null, true, null, null);
public static final TextStyle STRIKETHOUGH = new TextStyle(null, null, null, true, null);
public static final TextStyle OBFUSCATED = new TextStyle(null, null, null, null, true);
public static final TextStyle RESET = new TextStyle(false, false, false, false, false);
public static final TextStyle NONE = new TextStyle(null, null, null, null, null);
private Boolean bold;
private Boolean italic;
private Boolean underline;
private Boolean strikethrough;
private Boolean obfuscated;
public static Builder builder() {
return new TextStyle.Builder();
}
@NoArgsConstructor
@ToString
public static class Builder {
private Boolean bold;
private Boolean italic;
private Boolean underline;
private Boolean strikethrough;
private Boolean obfuscated;
public Builder bold(Boolean bold) {
this.bold = bold;
return this;
}
public Builder italic(Boolean italic) {
this.italic = italic;
return this;
}
public Builder underline(Boolean underline) {
this.underline = underline;
return this;
}
public Builder strikethrough(Boolean strikethrough) {
this.strikethrough = strikethrough;
return this;
}
public Builder obfuscated(Boolean obfuscated) {
this.obfuscated = obfuscated;
return this;
}
public Builder merge(TextStyle style) {
//@formatter:off
if (style.bold != null) this.bold = style.bold;
if (style.italic != null) this.italic = style.italic;
if (style.underline != null) this.underline = style.underline;
if (style.strikethrough != null) this.strikethrough = style.strikethrough;
if (style.obfuscated != null) this.obfuscated = style.obfuscated;
//@formatter:on
return this;
}
public TextStyle build() {
return new TextStyle(bold, italic, underline, strikethrough, obfuscated);
}
}
}

View File

@@ -0,0 +1,12 @@
package mc.protocol.packets;
import mc.protocol.buffer.NetByteBuf;
import mc.utils.pool.Passivable;
/**
* Пакеты отправляемые клиентом.
*/
public interface ClientSidePacket extends Packet, Passivable {
void readSelf(NetByteBuf netByteBuf);
}

View File

@@ -0,0 +1,46 @@
package mc.protocol.packets;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
/**
* Пинг-пакет.
*
* <p>Эхо-пакет, которым проверяется качество соединения между <b>Клиентом</b> и <b>Сервером</b>.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |---------|------|------------------------|
* | Payload | Long | Любое уникальное число |
* </pre>
*
* <p>По спецификации:</p>
* <oi>
* <li>если Сервер не ответил Клиенту в течении 20 секунд, Клиент отключается и выдаёт ошибку "Timed out";</li>
* <li>если Клиент не отвечает Серверу в течении 30 секунд, Сервер отключает Клиента.</li>
* </oi>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Keep_Alive_.28clientbound.29">Keep Alive (clientbound)</a>
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Keep_Alive_.28serverbound.29">Keep Alive (serverbound)</a>
*/
@Data
public class KeepAlivePacket implements ClientSidePacket, ServerSidePacket {
private long payload;
@Override
public void readSelf(NetByteBuf netByteBuf) {
payload = netByteBuf.readLong();
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeLong(payload);
}
@Override
public void passivate() {
this.payload = 0;
}
}

View File

@@ -0,0 +1,19 @@
package mc.protocol.packets;
/**
* Сетевой пакет.
*
* <p>Структура</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|-------------------------------------------|
* | SIZE | VarInt | = sizeOf(PACKET ID) + sizeOf(PACKET DATA) |
* | PACKET ID | VarInt | |
* | PACKET DATA | bytes | |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Without_compression">Packet without compression</a>
*/
public interface Packet {
}

View File

@@ -0,0 +1,11 @@
package mc.protocol.packets;
import mc.protocol.buffer.NetByteBuf;
/**
* Пакеты отправляемые сервером.
*/
public interface ServerSidePacket extends Packet {
void writeSelf(NetByteBuf netByteBuf);
}

View File

@@ -0,0 +1,32 @@
package mc.protocol.packets;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.State;
import mc.protocol.buffer.NetByteBuf;
@NoArgsConstructor
@Data
@ToString(exclude = "rawData")
public class UnknownPacket implements ClientSidePacket {
private State state;
private int id;
private int dataSize;
private byte[] rawData;
@Override
public void readSelf(NetByteBuf netByteBuf) {
rawData = new byte[dataSize];
netByteBuf.readBytes(rawData);
}
@Override
public void passivate() {
this.state = null;
this.id = 0;
this.dataSize = 0;
this.rawData = null;
}
}

View File

@@ -0,0 +1,57 @@
package mc.protocol.packets.handshaking.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.State;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
/**
* Handshake packet.
*
* <p>Данный пакет заставляет сервер переключить текущий {@link State}</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |------------------|----------------|----------------------------------------------|
* | Protocol version | VarInt | Версия протокола [1] |
* | Server address | Stirng (255) | Hostname или IP |
* | Server port | Unsigned Short | Порт сервера |
* | Next state | VarInt | ID State на который необходимо переключиться |
*
* [1] - <a href="https://wiki.vg/Protocol_version_numbers">Protocol version numbers</a>
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Handshake">Handshake</a>
* @see State
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class HandshakePacket implements ClientSidePacket {
private int protocolVersion;
private String host;
private int port;
private State nextState;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.protocolVersion = netByteBuf.readVarInt();
this.host = netByteBuf.readString(255);
this.port = netByteBuf.readUnsignedShort();
this.nextState = State.getById(netByteBuf.readVarInt());
}
@Override
public void passivate() {
this.protocolVersion = 0;
this.host = null;
this.port = 0;
this.nextState = null;
}
}

View File

@@ -0,0 +1,42 @@
package mc.protocol.packets.login.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
/**
* Login start packet.
*
* <p>Начало авторизации.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------|--------|------------------|
* | Name | String | Имя/Логин игрока |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Login_Start">Login start</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class LoginStartPacket implements ClientSidePacket {
private String name;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.name = netByteBuf.readString();
}
@Override
public void passivate() {
this.name = null;
}
}

View File

@@ -0,0 +1,43 @@
package mc.protocol.packets.login.server;
import lombok.Data;
import mc.protocol.State;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.model.text.Text;
import mc.protocol.packets.ServerSidePacket;
/**
* Diconnect packet.
*
* <p>Отключение клиента сервером с указанием причины.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|----------------------------------|
* | JSON Reason | Text | Причина отключения. Опционально. |
* </pre>
*
* <p>Пример JSON Reason</p>
* <pre>
* {
* "text": "foo"
* }
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Disconnect_.28login.29">Disconnect (login)</a>
* @see State
*/
@Data
public class DisconnectPacket implements ServerSidePacket {
/**
* Причина отключения.
*/
private Text reason;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeText(reason);
}
}

View File

@@ -0,0 +1,33 @@
package mc.protocol.packets.login.server;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
import java.util.UUID;
/**
* Подтверждение успешного логина.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |----------|-------------|-------------------------------|
* | UUID | String (36) | Уникальный ID игрока |
* | Username | String (16) | Имя игрока, выданное сервером |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Login_Success">Login Success</a>
*/
@Data
public class LoginSuccessPacket implements ServerSidePacket {
private UUID uuid;
private String name;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeString(this.uuid.toString());
netByteBuf.writeString(this.name);
}
}

View File

@@ -0,0 +1,68 @@
package mc.protocol.packets.play.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.model.Location;
import mc.protocol.model.Look;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.pool.LocationObjectPool;
import mc.protocol.pool.LookObjectPool;
/**
* Клиент сообщает о движении и повороте головы Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-----------|---------|------------------------------------------------------------|
* | X | Double | Абсолютная позиция по X |
* | Y | Double | Абсолютная позиция по Y. |
* | | | Имеется ввиду позиция ног. Голова находиться выше на 1.62f |
* | Z | Double | Абсолютная позиция по Z |
* | Yaw | Float | Абсолютный поворот головы по OX, в градусах |
* | Pitch | Float | Абсолютный поворот головы по OY, в градусах |
* | On Ground | Boolean | true, если Игрок находится на земле |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Position_And_Look_.28serverbound.29">Player Position And Look (serverbound)</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class CPlayerPositionAndLookPacket implements ClientSidePacket {
private Location position;
private Look look;
private boolean onGround;
@Override
public void readSelf(NetByteBuf netByteBuf) {
double x = netByteBuf.readDouble();
double y = netByteBuf.readDouble();
double z = netByteBuf.readDouble();
this.position = LocationObjectPool.borrowObject().set(x, y, z);
float yaw = netByteBuf.readFloat();
float pitch = netByteBuf.readFloat();
this.look = LookObjectPool.borrowObject().set(yaw, pitch);
this.onGround = netByteBuf.readBoolean();
}
@Override
public void passivate() {
LocationObjectPool.returnObject(this.position);
this.position = null;
LookObjectPool.returnObject(this.look);
this.look = null;
this.onGround = false;
}
public double getYPositionHead() {
return this.position.getY() + 1.62f;
}
}

View File

@@ -0,0 +1,104 @@
package mc.protocol.packets.play.client;
import lombok.*;
import mc.protocol.utils.ChatMode;
import mc.protocol.utils.MainHand;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
/**
* Client settings packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------------- |---------------|---------------------------------------------------|
* | Locale | String (16) | например en_gb |
* | View Distance | Byte | Дистанция отрисовки со стороны Клиента, в чанках. |
* | Chat Mode | VarInt | 0: enabled |
* | | | 1: commands only |
* | | | 2: hidden |
* | | | [1] |
* | Chat Colors | Boolean | “Colors” multiplayer setting (???) |
* | Displayed Skin Parts | Unsigned Byte | битовая маска отображения скина. См. ниже |
* | Main Hand | VarInt | 0: Left |
* | | | 1: Right |
*
* [1] - <a href="https://wiki.vg/index.php?title=Chat&oldid=13165#Processing_chat">Processing chat</a>
* </pre>
*
* <p>Биты "Displayed Skin Parts"</p>
* <pre>
* Bit 0 (0x01): Плащ (Cape)
* Bit 1 (0x02): Рубашка (Jacket)
* Bit 2 (0x04): Левый рукав (Left Sleeve)
* Bit 3 (0x08): Правый рукав (Right Sleeve)
* Bit 4 (0x10): Левая штанина (Left Pants Leg)
* Bit 5 (0x20): Правая штанина (Right Pants Leg)
* Bit 6 (0x40): Шлем (Hat)
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Client_Settings">Client Settings</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class ClientSettingsPacket implements ClientSidePacket {
private String locale;
private int viewDistance;
private ChatMode chatMode;
private boolean chatColors;
@SuppressWarnings("java:S116")
private int $displayedSkinPartsBitMask;
private MainHand mainHand;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.locale = netByteBuf.readString(16);
this.viewDistance = netByteBuf.readByte();
this.chatMode = ChatMode.valueById(netByteBuf.readVarInt());
this.chatColors = netByteBuf.readBoolean();
this.$displayedSkinPartsBitMask = netByteBuf.readUnsignedByte();
this.mainHand = MainHand.valueById(netByteBuf.readVarInt());
}
@Override
public void passivate() {
this.locale = null;
this.viewDistance = 0;
this.chatMode = null;
this.chatColors = false;
this.$displayedSkinPartsBitMask = 0;
this.mainHand = null;
}
public boolean isCapeEnabled() {
return ($displayedSkinPartsBitMask & 0x01) > 0;
}
public boolean isJacketEnabled() {
return ($displayedSkinPartsBitMask & 0x02) > 0;
}
public boolean isLeftSleeveEnabled() {
return ($displayedSkinPartsBitMask & 0x04) > 0;
}
public boolean isRightSleeveEnabled() {
return ($displayedSkinPartsBitMask & 0x08) > 0;
}
public boolean isLeftPantsEnabled() {
return ($displayedSkinPartsBitMask & 0x10) > 0;
}
public boolean isRightPantsEnabled() {
return ($displayedSkinPartsBitMask & 0x20) > 0;
}
public boolean isHatEnabled() {
return ($displayedSkinPartsBitMask & 0x40) > 0;
}
}

View File

@@ -0,0 +1,77 @@
package mc.protocol.packets.play.client;
import lombok.*;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
import javax.annotation.Nullable;
/**
* Entity Action packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |------------|--------|-------------------------------------------|
* | Entity ID | VarInt | ID игрока |
* | Action ID | VarInt | ID действия |
* | Jump Boost | VarInt | Используется только при "Action ID" = 5. |
* | | | В этом случае значение будет от 0 до 100. |
* | | | В остальных случаях значение 0. |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Entity_Action" target="_top">Entity Action</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class EntityActionPacket implements ClientSidePacket {
private Integer entityId;
private Action action;
private Integer jumpBoost;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.entityId = netByteBuf.readVarInt();
int actionId = netByteBuf.readVarInt();
this.jumpBoost = netByteBuf.readVarInt();
this.action = Action.valueOfCode(actionId);
}
@Override
public void passivate() {
this.entityId = null;
this.action = null;
this.jumpBoost = null;
}
@RequiredArgsConstructor
public enum Action {
START_SNEAKING(0),
STOP_SNEAKING(1),
LEAVE_BED(2),
START_SPRINTING(3),
STOP_SPRINTING(4),
START_JUMP_WITH_HORSE(5),
STOP_JUMP_WITH_HORSE(6),
OPEN_HORSE_INVENTORY(7),
START_FLYING_WITH_ELYTRA(8);
@Nullable
public static Action valueOfCode(int code) {
for (Action action : Action.values()) {
if (action.code == code) {
return action;
}
}
return null;
}
@Getter
private final int code;
}
}

View File

@@ -0,0 +1,50 @@
package mc.protocol.packets.play.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.model.Look;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.pool.LookObjectPool;
/**
* Клиент сообщает о повороте головы Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-----------|---------|---------------------------------------------|
* | Yaw | Float | Абсолютный поворот головы по OX, в градусах |
* | Pitch | Float | Абсолютный поворот головы по OY, в градусах |
* | On Ground | Boolean | true, если Игрок находится на земле |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Look">Player Look</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class PlayerLookPacket implements ClientSidePacket {
private Look look;
private boolean onGround;
@Override
public void readSelf(NetByteBuf netByteBuf) {
float yaw = netByteBuf.readFloat();
float pitch = netByteBuf.readFloat();
this.look = LookObjectPool.borrowObject().set(yaw, pitch);
this.onGround = netByteBuf.readBoolean();
}
@Override
public void passivate() {
LookObjectPool.returnObject(this.look);
this.look = null;
this.onGround = false;
}
}

View File

@@ -0,0 +1,57 @@
package mc.protocol.packets.play.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.model.Location;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.pool.LocationObjectPool;
/**
* Клиент сообщает о движении Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-----------|---------|-------------------------------------|
* | X | Double | Абсолютная позиция по X |
* | Feet Y | Double | Абсолютная позиция ног по Y. |
* | | | Голова находиться выше на 1.62f |
* | Z | Double | Абсолютная позиция по Z |
* | On Ground | Boolean | true, если Игрок находится на земле |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Position">Player Position</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class PlayerPositionPacket implements ClientSidePacket {
private Location position;
private boolean onGround;
@Override
public void readSelf(NetByteBuf netByteBuf) {
double x = netByteBuf.readDouble();
double y = netByteBuf.readDouble();
double z = netByteBuf.readDouble();
this.position = LocationObjectPool.borrowObject().set(x, y, z);
this.onGround = netByteBuf.readBoolean();
}
@Override
public void passivate() {
LocationObjectPool.returnObject(this.position);
this.position = null;
this.onGround = false;
}
public double getYPositionHead() {
return this.position.getY() + 1.62f;
}
}

View File

@@ -0,0 +1,48 @@
package mc.protocol.packets.play.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
/**
* Plugin Message packet.
*
* <p>Канал связи для модов и плагинов.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------------|-------------|------------------|
* | Channel name | String (20) | Название канала |
* | Data | Byte array | Любые данные |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Plugin_Message_.28serverbound.29">Plugin Message (serverbound)</a>
* @see <a href="https://wiki.vg/index.php?title=Plugin_channels&oldid=14089">Plugin channels</a>
* @see <a href="https://dinnerbone.com/blog/2012/01/13/minecraft-plugin-channels-messaging/">Minecraft Plugin Channels + Messaging</a>
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class PluginMessagePacket implements ClientSidePacket {
private String channelName;
private byte[] rawData;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.channelName = netByteBuf.readString(20);
this.rawData = new byte[netByteBuf.readableBytes()];
netByteBuf.readBytes(this.rawData);
}
@Override
public void passivate() {
this.channelName = null;
this.rawData = null;
}
}

View File

@@ -0,0 +1,41 @@
package mc.protocol.packets.play.client;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.packets.play.server.SPlayerPositionAndLookPacket;
/**
* Teleport сonfirm packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|------------------------------------------------------------|
* | Teleport ID | VarInt | ID, который был выдан пакетом {@link SPlayerPositionAndLookPacket} |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=7368#Login_Start" target="_top">Login start</a>
* @see SPlayerPositionAndLookPacket
*/
@NoArgsConstructor
@Getter
@EqualsAndHashCode
@ToString
public class TeleportConfirmPacket implements ClientSidePacket {
private int teleportId;
@Override
public void readSelf(NetByteBuf netByteBuf) {
this.teleportId = netByteBuf.readVarInt();
}
@Override
public void passivate() {
this.teleportId = 0;
}
}

View File

@@ -0,0 +1,108 @@
package mc.protocol.packets.play.server;
import io.netty.buffer.Unpooled;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Данные чанка.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------------------------|------------- |------------------------------------------------------------------------------------|
* | Chunk X | Integer | Координаты чанка (координата блока, делённая на 16, округленная в меньшую сторону) |
* | Chunk Z | Integer | Координаты чанка (координата блока, делённая на 16, округленная в меньшую сторону) |
* | Is Full chunk | Boolean | См. Chunk Format |
* | Available Sections | VarInt | Битовая маска, где каждый бит - это часть чанка (0-15) |
* | Size of Data | VarInt | Размер поля "Data" |
* | Data | Byte array | Данные чанка. См. Chunk Format |
* | Number of block entities | VarInt | Количество элементов в поле "Block entities" |
* | Block entities | Array of NBT | Все сущности в чанке |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Chunk_Data">Chunk Data</a>
* @see <a href="https://wiki.vg/index.php?title=Chunk_Format&oldid=14135">Chunk Format</a>
*/
@Data
public class ChunkDataPacket implements ServerSidePacket {
private static NetByteBuf voidData;
private int x;
private int z;
@SuppressWarnings("java:S125")
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeInt(x);
netByteBuf.writeInt(z);
/* Временное отключение кода
netByteBuf.writeBoolean(true); // Is Full chunk
netByteBuf.writeVarInt(0b11111111); // Available Sections
NetByteBuf data = new NetByteBuf(Unpooled.buffer());
// <Data>
for (int i = 0; i < 16; i++) {
NetByteBuf dataBuff = new NetByteBuf(Unpooled.wrappedBuffer(new byte[4096]));
NetByteBuf blockLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf skyLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf biomes = new NetByteBuf(Unpooled.wrappedBuffer(new byte[256]));
// <Chunk Section>
data.writeUnsignedByte(13); // Bits Per Block
// <Palette>
data.writeUnsignedByte(0); // Palette Length (for direct)
// <Palette Data/>
// </Palette>
data.writeVarInt(dataBuff.readableBytes()); // Data Array Length
data.writeBytes(dataBuff); // Data Array
data.writeBytes(blockLight); // Block Light
data.writeBytes(skyLight); // Sky Light
// </Chunk Section>
data.writeBytes(biomes); // Biomes
}
// </Data>
netByteBuf.writeVarInt(data.readableBytes()); // Size of Data
netByteBuf.writeBytes(data); // Data
netByteBuf.writeVarInt(0); // Number of block entities
// write NBT's
*/
netByteBuf.writeBytes(voidData);
voidData.resetReaderIndex();
voidData.resetWriterIndex();
}
static {
voidData = new NetByteBuf(Unpooled.buffer());
voidData.writeBoolean(true); // Is Full chunk
voidData.writeVarInt(0b11111111); // Available Sections
NetByteBuf data = new NetByteBuf(Unpooled.buffer());
for (int i = 0; i < 16; i++) {
NetByteBuf dataBuff = new NetByteBuf(Unpooled.wrappedBuffer(new byte[4096]));
NetByteBuf blockLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf skyLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048]));
NetByteBuf biomes = new NetByteBuf(Unpooled.wrappedBuffer(new byte[256]));
data.writeUnsignedByte(13);
data.writeUnsignedByte(0);
data.writeVarInt(dataBuff.readableBytes());
data.writeBytes(dataBuff);
data.writeBytes(blockLight);
data.writeBytes(skyLight);
data.writeBytes(biomes);
}
voidData.writeVarInt(data.readableBytes());
voidData.writeBytes(data);
voidData.writeVarInt(0);
voidData.markReaderIndex();
voidData.markWriterIndex();
}
}

View File

@@ -0,0 +1,59 @@
package mc.protocol.packets.play.server;
import lombok.Data;
import mc.protocol.utils.Difficulty;
import mc.protocol.utils.GameMode;
import mc.protocol.utils.LevelType;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Join game packet.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |--------------------|---------------|----------------------------------------------------------------------|
* | Entity ID | Integer | ID сущности (игрока) |
* | Gamemode | Unsigned Byte | 0: Survival |
* | | | 1: Creative |
* | | | 2: Adventure |
* | | | 3: Spectator |
* | | | Bit 3 (0x8) is the hardcore flag. |
* | Dimension | Integer | -1: Nether |
* | | | 0: Overworld |
* | | | 1: End |
* | Difficulty | Unsigned Byte | 0: peaceful |
* | | | 1: easy |
* | | | 2: normal |
* | | | 3: hard |
* | Max Players | Unsigned Byte | Когда-то использовался клиентом для |
* | | | отображения списка игроков. Теперь не используется |
* | Level Type | String (16) | Принимает одно из значений: |
* | | | default, flat, largeBiomes, amplified, default_1_1 |
* | Reduced Debug Info | Boolean | Если true, то Клиент отображает меньше отладочной информации (в F3?) |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Join_Game">Join Game</a>
*/
@Data
public class JoinGamePacket implements ServerSidePacket {
private int entityId;
private GameMode gameMode;
private int dimension;
private Difficulty difficulty;
private LevelType levelType;
private boolean reducedDebugInfo;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeInt(entityId);
netByteBuf.writeUnsignedByte(gameMode.getId());
netByteBuf.writeInt(dimension);
netByteBuf.writeUnsignedByte(difficulty.getId());
netByteBuf.writeUnsignedByte(0); // Max Players, unused
netByteBuf.writeString(levelType.getType());
netByteBuf.writeBoolean(reducedDebugInfo);
}
}

View File

@@ -0,0 +1,60 @@
package mc.protocol.packets.play.server;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Характеристики игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |------------------------------|----------|-----------------------------------------|
* | Flags | Byte | Битовая маска флагов. См. ниже значения |
* | Flying Speed | Float | Скорость полёта |
* | Field of View (FOV) Modifier | Float | Поле зрения |
* </pre>
*
* <p>Флаги "Flags"</p>
* <pre>
* Bit 0x01 - Неуязвимость (Invulnerable)
* Bit 0x02 - В полёте (Flying)
* Bit 0x04 - Может летать (Allow Flying)
* Bit 0x08 - Creative Mode (Instant Break)
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Abilities_.28clientbound.29">Player Abilities</a>
*/
@Data
public class PlayerAbilitiesPacket implements ServerSidePacket {
@SuppressWarnings("java:S116")
private byte $flags = 0;
private float flyingSpeed;
private float fieldOfView;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeByte(this.$flags);
netByteBuf.writeFloat(this.flyingSpeed);
netByteBuf.writeFloat(this.fieldOfView);
}
//FIXME использование value значений
public void setInvulnerable(boolean value) {
this.$flags = (byte) (this.$flags | 0x01);
}
public void setFlying(boolean value) {
this.$flags = (byte) (this.$flags | 0x02);
}
public void setCatFly(boolean value) {
this.$flags = (byte) (this.$flags | 0x04);
}
public void setCreativeMode(boolean value) {
this.$flags = (byte) (this.$flags | 0x08);
}
}

View File

@@ -0,0 +1,83 @@
package mc.protocol.packets.play.server;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.model.Location;
import mc.protocol.model.Look;
import mc.protocol.packets.ServerSidePacket;
import mc.protocol.packets.play.client.TeleportConfirmPacket;
/**
* Установка позиции и угла осмотра Игрока.
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |-------------|--------|-----------------------------------------------------------------------------------|
* | X | Double | Абсолютная или относительная позиция по X. Зависит от "Flags" |
* | Y | Double | Абсолютная или относительная позиция по Y. Зависит от "Flags" |
* | Z | Double | Абсолютная или относительная позиция по Z. Зависит от "Flags" |
* | Yaw | Float | Абсолютный или относительный поворот головы по OX, в градусах. Зависит от "Flags" |
* | Pitch | Float | Абсолютный или относительный поворот головы по OY, в градусах. Зависит от "Flags" |
* | Flags | Byte | Битовая маска значений флагов. См. значения ниже |
* | Teleport ID | VarInt | ID для подтверждения клиентом перемещения Игрока |
* </pre>
*
* <p>Значения "Flags"</p>
* <pre>
* | Field | Bit |
* |-------|------|
* | X | 0x01 |
* | Y | 0x02 |
* | Z | 0x04 |
* | X_ROT | 0x08 |
* | Y_ROT | 0x10 |
* </pre>
*
* <p>Примечание от Dinnerbone про "Flags":</p>
* <i>"It's a bitfield, X/Y/Z/Y_ROT/X_ROT. If X is set, the x value is relative and not absolute."</i>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Player_Position_And_Look_.28clientbound.29">Player Position And Look (clientbound)</a>
* @see TeleportConfirmPacket
*/
@Data
public class SPlayerPositionAndLookPacket implements ServerSidePacket {
private Location position;
private Look look;
@SuppressWarnings("java:S116")
private byte $flags = 0;
private int teleportId;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeDouble(this.position.getX());
netByteBuf.writeDouble(this.position.getY());
netByteBuf.writeDouble(this.position.getZ());
netByteBuf.writeFloat(this.look.getYaw());
netByteBuf.writeFloat(this.look.getPitch());
netByteBuf.writeByte(this.$flags);
netByteBuf.writeVarInt(teleportId);
}
//FIXME использовать value значения
public void setFlagX(boolean value) {
this.$flags = (byte) (this.$flags | 0x01);
}
public void setFlagY(boolean value) {
this.$flags = (byte) (this.$flags | 0x02);
}
public void setFlagZ(boolean value) {
this.$flags = (byte) (this.$flags | 0x04);
}
public void setFlagXRot(boolean value) {
this.$flags = (byte) (this.$flags | 0x08);
}
public void setFlagYRot(boolean value) {
this.$flags = (byte) (this.$flags | 0x10);
}
}

View File

@@ -0,0 +1,41 @@
package mc.protocol.packets.play.server;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.model.Location;
import mc.protocol.packets.ServerSidePacket;
/**
* Спавн позиция игрока.
*
* <p>Используется призаходе игрока на сервер.</p>
*
* <p>Структура пакета</p>
* <pre>
* | FIELD | TYPE | NOTES |
* |----------|----------|-----------------------|
* | Location | Position | Локация спавна игрока |
* </pre>
*
* @see <a href="https://wiki.vg/index.php?title=Protocol&oldid=14204#Spawn_Position">Spawn Position</a>
*/
@Data
public class SpawnPositionPacket implements ServerSidePacket {
private Location spawn;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
long spawnSerialized =
((long) (floorDouble(spawn.getX()) & 0x3FFFFFF) << 38)
| ((long) (floorDouble(spawn.getY()) & 0xFFF) << 26)
| (floorDouble(spawn.getZ()) & 0x3FFFFFF);
netByteBuf.writeLong(spawnSerialized);
}
private static int floorDouble(double value) {
int i = (int) value;
return value < (double) i ? i - 1 : i;
}
}

View File

@@ -0,0 +1,34 @@
package mc.protocol.packets.status.client;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ClientSidePacket;
import mc.protocol.packets.ServerSidePacket;
/**
* Status server packet, request.
*
* <p>Клиент запрашивает получение информации о сервере</p>
*/
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@ToString
public class StatusServerRequestPacket implements ClientSidePacket, ServerSidePacket {
@Override
public void readSelf(NetByteBuf netByteBuf) {
// empty
}
@Override
public void writeSelf(NetByteBuf netByteBuf) {
// empty
}
@Override
public void passivate() {
// pass
}
}

View File

@@ -0,0 +1,59 @@
package mc.protocol.packets.status.server;
import lombok.Data;
import mc.protocol.buffer.NetByteBuf;
import mc.protocol.packets.ServerSidePacket;
/**
* Status server packet, response.
*
* <p>Информация о сервере</p>
*
* <p>Структура пакета
* <pre>
* | FIELD | TYPE | NOTES |
* |---------------|--------|-----------------------------------------|
* | JSON Response | String | Информация о сервере в JSON формате [1] |
*
* [1] - <a href="https://wiki.vg/index.php?title=Server_List_Ping&oldid=7555#Response" target="_top">Server List Ping: Response</a>
* </pre>
*
* <p>Пример JSON Response</p>
* <pre>
* {
* "version": {
* "name": "1.8.7",
* "protocol": 47
* },
* "players": {
* "max": 100,
* "online": 5,
* "sample": [
* {
* "name": "thinkofdeath",
* "id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20"
* }
* ]
* },
* "description": {
* "text": "Hello world"
* },
* "favicon": "data:image/png;base64,&lt;data&gt;"
* }
* </pre>
*
* <p><code>`$.favicon`</code> должен быть формата PNG и размеры 64x64 px</p>
*/
@Data
public class StatusServerResponse implements ServerSidePacket {
/**
* Информация о серере.
*/
private String info;
@Override
public void writeSelf(NetByteBuf netByteBuf) {
netByteBuf.writeString(info);
}
}

View File

@@ -0,0 +1,37 @@
package mc.protocol.pool;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import mc.protocol.model.Location;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LocationObjectPool extends BasePooledObjectFactory<Location> {
private static final ObjectPool<Location> instance = new GenericObjectPool<>(new LocationObjectPool());
@Override
public Location create() {
return new Location();
}
@Override
public PooledObject<Location> wrap(Location location) {
return new DefaultPooledObject<>(location);
}
@SneakyThrows
public static Location borrowObject() {
return instance.borrowObject();
}
@SneakyThrows
public static void returnObject(Location location) {
instance.returnObject(location);
}
}

View File

@@ -0,0 +1,37 @@
package mc.protocol.pool;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import mc.protocol.model.Look;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LookObjectPool extends BasePooledObjectFactory<Look> {
private static final ObjectPool<Look> instance = new GenericObjectPool<>(new LookObjectPool());
@Override
public Look create() {
return new Look();
}
@Override
public PooledObject<Look> wrap(Look look) {
return new DefaultPooledObject<>(look);
}
@SneakyThrows
public static Look borrowObject() {
return instance.borrowObject();
}
@SneakyThrows
public static void returnObject(Look look) {
instance.returnObject(look);
}
}

View File

@@ -0,0 +1,14 @@
package mc.protocol.pool;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import mc.protocol.packets.ClientSidePacket;
import mc.utils.pool.WideClassObjectPool;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class PacketObjectPool extends WideClassObjectPool<ClientSidePacket> {
@Getter
private static final PacketObjectPool instance = new PacketObjectPool();
}

View File

@@ -0,0 +1,20 @@
package mc.protocol.utils;
import javax.annotation.Nullable;
public enum ChatMode {
FULL,
COMMANDS_ONLY,
HIDDEN;
@Nullable
public static ChatMode valueById(int id) {
// а зачем усложнять?
//@formatter:off
if (id == 1) return FULL;
else if (id == 2) return COMMANDS_ONLY;
else if (id == 3) return HIDDEN;
else return null;
//@formatter:on
}
}

View File

@@ -0,0 +1,15 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum Difficulty {
PEACEFUL(0),
EASY(1),
NORMAL(2),
HARD(3);
private final int id;
}

View File

@@ -0,0 +1,15 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum GameMode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
SPECTATOR(3);
private final int id;
}

View File

@@ -0,0 +1,16 @@
package mc.protocol.utils;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum LevelType {
DEFAULT_TYPE("default"),
FLAT("flat"),
LARGE_BIOMES("largeBiomes"),
AMPLIFIED("amplified"),
DEFAULT_1_1("default_1_1");
private final String type;
}

View File

@@ -0,0 +1,18 @@
package mc.protocol.utils;
import javax.annotation.Nullable;
public enum MainHand {
LEFT,
RIGHT;
@Nullable
public static MainHand valueById(int id) {
// а зачем усложнять?
//@formatter:off
if (id == 0) return LEFT;
else if (id == 1) return RIGHT;
else return null;
//@formatter:on
}
}

View File

@@ -0,0 +1,23 @@
package mc.protocol.utils;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class Table<C, R, V> {
private final Map<C, Map<R, V>> map = new HashMap<>();
@Nullable
public V getColumnAndRow(C column, R row) {
if (!map.containsKey(column)) {
return null;
}
return map.get(column).get(row);
}
public void put(C column, R row, V value) {
map.computeIfAbsent(column, c -> new HashMap<>()).put(row, value);
}
}

View File

@@ -0,0 +1,7 @@
package mc.protocol.world;
public interface Chunk {
int getX();
int getZ();
}

View File

@@ -0,0 +1,13 @@
package mc.protocol.world;
import mc.protocol.model.Location;
import mc.protocol.utils.LevelType;
public interface World {
LevelType getLevelType();
Location getSpawn();
Chunk getChunk(int x, int z);
}

View File

@@ -0,0 +1,305 @@
package mc.protocol.buffer;
import io.netty.buffer.Unpooled;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class NetByteBufReadTest {
private static Random random;
private ByteArrayOutputStream baos;
@BeforeEach
void setUp() {
random = new Random(System.currentTimeMillis());
baos = new ByteArrayOutputStream();
}
@ParameterizedTest
@MethodSource("paramsReadBoolean")
void readBoolean(byte sourceByte, boolean expectedValue) {
baos.write(sourceByte);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readBoolean());
}
@Test
void readByte() {
byte[] bytes = new byte[1];
random.nextBytes(bytes);
baos.write(bytes[0]);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(bytes[0], netByteBuf.readByte());
}
@ParameterizedTest
@MethodSource("paramsReadUnsignedByte")
void readUnsignedByte(byte sourceByte, int expectedValue) {
baos.write(sourceByte);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readUnsignedByte());
}
@Test
void readShort() throws IOException {
int value = Integer.valueOf(random.nextInt()).shortValue();
new DataOutputStream(baos).writeShort(value);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(value, netByteBuf.readShort());
}
@Test
void readUnsignedShort() throws IOException {
int value = 32768;
new DataOutputStream(baos).writeShort(value);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(value, netByteBuf.readUnsignedShort());
}
@ParameterizedTest
@MethodSource("paramsReadString")
void readString(String string) throws IOException {
final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = new byte[strBytes.length + 1];
bytes[0] = (byte) string.codePoints().count(); // допустим, что размер поместился в один байт
System.arraycopy(strBytes, 0, bytes, 1, strBytes.length);
baos.write(bytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(string, netByteBuf.readString());
}
@Test
void readString_overSize() throws IOException {
String string = "123";
final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = new byte[strBytes.length + 1];
final int length = string.length();
bytes[0] = (byte) (length + 1); // допустим, что размер поместился в один байт
System.arraycopy(strBytes, 0, bytes, 1, strBytes.length);
baos.write(bytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertThrows(NetIOException.class, () -> netByteBuf.readString(length));
}
@Test
void readString_lessZero() throws IOException {
String string = "123";
final byte[] strBytes = string.getBytes(StandardCharsets.UTF_8);
final byte[] bytes = new byte[strBytes.length + 5];
bytes[0] = (byte) 0xFF;
bytes[1] = (byte) 0xFF;
bytes[2] = (byte) 0xFF;
bytes[3] = (byte) 0xFF;
bytes[4] = (byte) 0x0F;
System.arraycopy(strBytes, 0, bytes, 5, strBytes.length);
baos.write(bytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertThrows(NetIOException.class, () -> netByteBuf.readString(-1));
}
@ParameterizedTest
@MethodSource("paramsReadVarInt")
void readVarInt(byte[] sourceBytes, int expectedValue) throws IOException {
baos.write(sourceBytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readVarInt());
}
@Test
void readVarInt_tooBig() throws IOException {
baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x0F });
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(-1, netByteBuf.readVarInt());
}
@ParameterizedTest
@MethodSource({"paramsReadVarInt", "paramsReadVarLong"})
void readVarLong(byte[] sourceBytes, long expectedValue) throws IOException {
baos.write(sourceBytes);
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(expectedValue, netByteBuf.readVarLong());
}
@Test
void readVarLong_tooBig() throws IOException {
baos.write(new byte[]{ (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0x0F });
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(-1, netByteBuf.readVarLong());
}
@Test
void readUUID() throws IOException {
final UUID uuid = UUID.randomUUID();
final long mostSignificantBits = uuid.getMostSignificantBits();
final long leastSignificantBits = uuid.getLeastSignificantBits();
baos.write(new byte[]{
(byte) ((mostSignificantBits >>> 56) & 0xFF),
(byte) ((mostSignificantBits >>> 48) & 0xFF),
(byte) ((mostSignificantBits >>> 40) & 0xFF),
(byte) ((mostSignificantBits >>> 32) & 0xFF),
(byte) ((mostSignificantBits >>> 24) & 0xFF),
(byte) ((mostSignificantBits >>> 16) & 0xFF),
(byte) ((mostSignificantBits >>> 8) & 0xFF),
(byte) (mostSignificantBits & 0xFF)
});
baos.write(new byte[]{
(byte) ((leastSignificantBits >>> 56) & 0xFF),
(byte) ((leastSignificantBits >>> 48) & 0xFF),
(byte) ((leastSignificantBits >>> 40) & 0xFF),
(byte) ((leastSignificantBits >>> 32) & 0xFF),
(byte) ((leastSignificantBits >>> 24) & 0xFF),
(byte) ((leastSignificantBits >>> 16) & 0xFF),
(byte) ((leastSignificantBits >>> 8) & 0xFF),
(byte) (leastSignificantBits & 0xFF)
});
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(uuid, netByteBuf.readUUID());
}
@Test
void readBytes() throws IOException {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
baos.write(bytes);
byte[] actualBytes = new byte[128];
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
assertEquals(bytes.length, netByteBuf.readableBytes());
netByteBuf.readBytes(actualBytes);
assertArrayEquals(bytes, actualBytes);
assertEquals(0, netByteBuf.readableBytes());
}
@Test
void read_offset() throws IOException {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
baos.write(bytes);
byte[] buff = new byte[128];
NetByteBuf netByteBuf = new NetByteBuf(Unpooled.wrappedBuffer(baos.toByteArray()));
netByteBuf.readBytes(buff, 3, 11);
byte[] expectedBytes = new byte[11];
System.arraycopy(bytes, 0, expectedBytes, 0, 11);
byte[] actualBytes = new byte[11];
System.arraycopy(buff, 3, actualBytes, 0, 11);
assertArrayEquals(expectedBytes, actualBytes);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadBoolean() {
return Stream.of(
Arguments.of((byte) 0x00, false),
Arguments.of((byte) 0x01, true)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadUnsignedByte() {
return Stream.of(
Arguments.of((byte) 30, 30),
Arguments.of((byte) (0xFF & 130), 130)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadString() {
return Stream.of(
Arguments.of(""),
Arguments.of("Latin"),
Arguments.of("Кириллица"),
Arguments.of("العربية"),
Arguments.of("ﬦﬣﬡ"), // Алфавитные формы представления
Arguments.of("\uD800\uDD07") // Эгейские цифры, [один]
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadVarInt() {
return Stream.of(
Arguments.of(new byte[]{ 0x78 }, 120),
Arguments.of(new byte[]{ (byte) 0xE0, 0x5D }, 12000),
Arguments.of(new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }, 120000),
Arguments.of(new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }, 120_000_000),
Arguments.of(new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 }, 1_200_000_000)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsReadVarLong() {
return Stream.of(
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0xF0, (byte) 0x85, (byte) 0xDA, 0x2C },
12_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0xE0, (byte) 0xBA, (byte) 0x84, (byte) 0xBF, 0x03 },
120_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xF3, (byte) 0xBD, (byte) 0x9F, (byte) 0xDD,
0x02 },
12_000_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xEC, (byte) 0xAD, (byte) 0xCC, (byte) 0xEC,
(byte) 0x90, 0x02},
1_200_000_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xB0, (byte) 0xE8, (byte) 0xD3, (byte) 0xEB,
(byte) 0x94, (byte) 0xD5, 0x01 },
120_000_000_000_000_000L),
Arguments.of(
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01 },
Long.MIN_VALUE)
);
}
}

View File

@@ -0,0 +1,260 @@
package mc.protocol.buffer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class NetByteBufWriteTest {
private static Random random;
@BeforeAll
static void setUp() {
random = new Random(System.currentTimeMillis());
}
@ParameterizedTest
@MethodSource("paramsWriteBoolean")
void writeBoolean(boolean sourceValue, byte expectedByte) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeBoolean(sourceValue);
assertEquals(expectedByte, byteBuf.array()[0]);
}
@ParameterizedTest
@MethodSource("paramsWriteByte")
void writeByte(byte sourceValue, byte expectedByte) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeByte(sourceValue);
assertEquals(expectedByte, byteBuf.array()[0]);
}
@Test
void writeUnsignedByte() {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeUnsignedByte(129);
assertEquals(129, netByteBuf.readUnsignedByte());
}
@ParameterizedTest
@MethodSource("paramsWriteString")
void writeString(String string, int exceptedLength) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeString(string);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
int actualLength = actualArray[0]; // допустим, что размер поместился в один байт
assertEquals(exceptedLength, actualLength);
byte[] dataBytes = new byte[actualArray.length - 1];
System.arraycopy(actualArray, 1, dataBytes, 0, dataBytes.length);
assertEquals(string, new String(dataBytes, StandardCharsets.UTF_8));
}
//возможно этот тест нужно перенести в NetByteBufReadTest
@Test
void writeString_overSize() {
String overSizeString = RandomStringUtils.randomAscii(Short.MAX_VALUE + Short.MAX_VALUE);
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeString(overSizeString);
NetByteBuf netByteBuf2 = new NetByteBuf(byteBuf.copy());
String actualString = netByteBuf2.readString();
String expectedString = overSizeString.substring(0, Short.MAX_VALUE);
assertEquals(expectedString, actualString);
}
@ParameterizedTest
@MethodSource("paramsWriteVarInt")
void writeVarInt(int sourceValue, byte[] expectedBytes) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeVarInt(sourceValue);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(expectedBytes, actualArray);
}
@ParameterizedTest
@MethodSource({ "paramsWriteVarInt", "paramsWriteVarLong" })
void writeVarLong(long sourceValue, byte[] expectedBytes) {
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeVarLong(sourceValue);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(expectedBytes, actualArray);
}
@Test
void writeUUID() {
final UUID uuid = UUID.randomUUID();
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeUUID(uuid);
final long mostSignificantBits = uuid.getMostSignificantBits();
final long leastSignificantBits = uuid.getLeastSignificantBits();
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(new byte[]{
(byte) ((mostSignificantBits >>> 56) & 0xFF),
(byte) ((mostSignificantBits >>> 48) & 0xFF),
(byte) ((mostSignificantBits >>> 40) & 0xFF),
(byte) ((mostSignificantBits >>> 32) & 0xFF),
(byte) ((mostSignificantBits >>> 24) & 0xFF),
(byte) ((mostSignificantBits >>> 16) & 0xFF),
(byte) ((mostSignificantBits >>> 8) & 0xFF),
(byte) (mostSignificantBits & 0xFF),
(byte) ((leastSignificantBits >>> 56) & 0xFF),
(byte) ((leastSignificantBits >>> 48) & 0xFF),
(byte) ((leastSignificantBits >>> 40) & 0xFF),
(byte) ((leastSignificantBits >>> 32) & 0xFF),
(byte) ((leastSignificantBits >>> 24) & 0xFF),
(byte) ((leastSignificantBits >>> 16) & 0xFF),
(byte) ((leastSignificantBits >>> 8) & 0xFF),
(byte) (leastSignificantBits & 0xFF) },
actualArray);
}
@Test
void writeBytes() {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeBytes(bytes);
byte[] actualArray = netByteBuf.copy(0, netByteBuf.readableBytes()).array();
assertArrayEquals(bytes, actualArray);
}
@Test
void write_offset() {
byte[] bytes = new byte[128];
random.nextBytes(bytes);
ByteBuf byteBuf = Unpooled.buffer();
NetByteBuf netByteBuf = new NetByteBuf(byteBuf);
netByteBuf.writeBytes(bytes, 3, 11);
byte[] actualBytes = new byte[11];
System.arraycopy(byteBuf.array(), 0, actualBytes, 0, 11);
byte[] expectedBytes = new byte[11];
System.arraycopy(bytes, 3, expectedBytes, 0, 11);
assertArrayEquals(expectedBytes, actualBytes);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteBoolean() {
return Stream.of(
Arguments.of(false, (byte) 0x00),
Arguments.of(true, (byte) 0x01)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteByte() {
byte b = Integer.valueOf(random.nextInt()).byteValue();
return Stream.of(
Arguments.of(b, b),
Arguments.of((byte) 128, (byte) -128)
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteString() {
return Stream.of(
Arguments.of("", 0),
Arguments.of("Latin", 5),
Arguments.of("Кириллица", 37),
// (9) -> "Кириллица"(18) => 18*2=36 (37?)
Arguments.of("العربية", 30),
// (7) -> "Ш§Щ„Ш№Ш±ШЁЩЉШ©"(14) => 14*2=28 (30?)
Arguments.of("ﬦﬣﬡ", 18), // Алфавитные формы представления
// (3) -> "ﬦﬣﬡ"(9) => 9*2=18
Arguments.of("\uD800\uDD07", 4) // Эгейские цифры, [один]
// (1) -> "𐄇" => ...4!
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteVarInt() {
return Stream.of(
Arguments.of(120, new byte[]{ 0x78 }),
Arguments.of(12000, new byte[]{ (byte) 0xE0, 0x5D }),
Arguments.of(120000, new byte[]{ (byte) 0xC0, (byte) 0xA9, 0x07 }),
Arguments.of(120000000, new byte[]{ (byte) 0x80, (byte) 0x9C, (byte) 0x9C, (byte) 0x39 }),
Arguments.of(1200000000, new byte[]{ (byte) 0x80, (byte) 0x98, (byte) 0x9A, (byte) 0xBC, 0x04 })
);
}
@SuppressWarnings("unused")
private static Stream<Arguments> paramsWriteVarLong() {
return Stream.of(
Arguments.of(
12_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0xF0, (byte) 0x85, (byte) 0xDA, 0x2C }),
Arguments.of(
120_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0xE0, (byte) 0xBA, (byte) 0x84, (byte) 0xBF, 0x03 }),
Arguments.of(
12_000_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xF3, (byte) 0xBD, (byte) 0x9F, (byte) 0xDD,
0x02 }),
Arguments.of(
1_200_000_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xEC, (byte) 0xAD, (byte) 0xCC, (byte) 0xEC,
(byte) 0x90, 0x02 }),
Arguments.of(
120_000_000_000_000_000L,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0xB0, (byte) 0xE8, (byte) 0xD3, (byte) 0xEB,
(byte) 0x94, (byte) 0xD5, 0x01 }),
Arguments.of(
Long.MIN_VALUE,
new byte[]{ (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80,
(byte) 0x80, (byte) 0x80, (byte) 0x80, 0x01 })
);
}
}

View File

@@ -0,0 +1,59 @@
package mc.protocol.handler.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class ProtocolSplitterTest {
byte[] expectedPacket1Bytes;
byte[] expectedPacket2Bytes;
byte[] bytes;
@Test
void split() {
prepare();
ByteBuf inputBuffer = Unpooled.wrappedBuffer(bytes);
List<Object> outputObject = new ArrayList<>();
ProtocolSplitter protocolSplitter = new ProtocolSplitter();
protocolSplitter.decode(null, inputBuffer, outputObject);
assertEquals(2, outputObject.size());
assertTrue(outputObject.get(0) instanceof ByteBuf);
assertArrayEquals(expectedPacket1Bytes, ((ByteBuf)outputObject.get(0)).array());
assertTrue(outputObject.get(1) instanceof ByteBuf);
assertArrayEquals(expectedPacket2Bytes, ((ByteBuf)outputObject.get(1)).array());
}
private void prepare() {
/*
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 10 00 d4 02 09 31 32 37 2e 30 2e 30 2e 31 63 dd |.....127.0.0.1c.|
|00000010| 01 01 00 |... |
+--------+-------------------------------------------------+----------------+
*/
expectedPacket1Bytes = new byte[]{
0x00, (byte) 0xd4, 0x02, 0x09, 0x31, 0x32, 0x37, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x63,
(byte) 0xdd, 0x01
};
expectedPacket2Bytes = new byte[]{
0x00
};
bytes = new byte[2 + expectedPacket1Bytes.length + expectedPacket2Bytes.length];
bytes[0] = 0x10;
System.arraycopy(expectedPacket1Bytes, 0,
bytes, 1, expectedPacket1Bytes.length);
bytes[1 + expectedPacket1Bytes.length] = 0x01;
System.arraycopy(expectedPacket2Bytes, 0,
bytes, 2 + expectedPacket1Bytes.length, expectedPacket2Bytes.length);
}
}