diff --git a/build.gradle b/build.gradle index a1dba4e..efb5d0b 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,9 @@ dependencies { /* LOGGER */ compile (group: 'org.slf4j', name: 'slf4j-api', version: slf4j_version) + /* COMPONENTS */ + compile (group: 'com.google.guava', name: 'guava', version: '28.0-jre') + /* LOMBOK */ annotationProcessor (group: 'org.projectlombok', name: 'lombok', version: lombok_version) compileOnly (group: 'org.projectlombok', name: 'lombok', version: lombok_version) diff --git a/src/main/java/mc/protocol/DecoderException.java b/src/main/java/mc/protocol/DecoderException.java new file mode 100644 index 0000000..ac45ea2 --- /dev/null +++ b/src/main/java/mc/protocol/DecoderException.java @@ -0,0 +1,8 @@ +package mc.protocol; + +public class DecoderException extends RuntimeException { + + public DecoderException(String message) { + super(message); + } +} diff --git a/src/main/java/mc/protocol/NetInputStream.java b/src/main/java/mc/protocol/NetInputStream.java new file mode 100644 index 0000000..968dfb8 --- /dev/null +++ b/src/main/java/mc/protocol/NetInputStream.java @@ -0,0 +1,106 @@ +package mc.protocol; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public abstract class NetInputStream extends InputStream { + + public boolean readBoolean() { + return readByte() >= 0x01; + } + + @Override + public int read() throws IOException { + return readByte(); + } + + // Unsigned Byte [1] + + public int readUnsignedShort() { + return readShort() & 0xFFFF; + } + + // Int [4] + + // Long [8] + + // Float [4] + + // Double [8] + + public String readString() { + return readString(Short.MAX_VALUE); + } + + public String readString(int maxLength) { + int length = readVarInt(); + + if (length == 0) { + return ""; + } else if (length > maxLength) { + throw new DecoderException("String length exceeds maximum length: " + length + " > " + maxLength); + } else if (length < 0) { + throw new DecoderException("String length less zero!"); + } + + byte[] bytes = new byte[length]; + readBytes(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + + // Chat + + // Identifier + + public int readVarInt() { + int numRead = 0; + int result = 0; + byte read; + do { + if ((numRead + 1) > 5) { + //FIXME выводить в лог предупреждение + break; // VarInt is too big + } + read = readByte(); + int value = (read & 0b01111111); + result |= (value << (7 * numRead)); + + numRead++; + } while ((read & 0b10000000) != 0); + + return result; + } + + // VarLong + + // Entity Metadata + + // Slot + + // NBT Tag + + // Position [8] + + // Angle [1] + + // UUID [16] + + public int readBytes(byte[] buffer) { + return readBytes(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer) throws IOException { + return readBytes(buffer); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + return readBytes(buffer, offset, length); + } + + public abstract byte readByte(); + public abstract int readBytes(byte[] buffer, int offset, int lengtn); + public abstract int readShort(); +} diff --git a/src/main/java/mc/protocol/NetOutputStream.java b/src/main/java/mc/protocol/NetOutputStream.java new file mode 100644 index 0000000..aa0ca68 --- /dev/null +++ b/src/main/java/mc/protocol/NetOutputStream.java @@ -0,0 +1,89 @@ +package mc.protocol; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public abstract class NetOutputStream extends OutputStream { + + public void writeBoolean(boolean value) { + writeByte(value ? 0x01 : 0x00); + } + + @Override + public void write(int b) throws IOException { + writeByte(b); + } + + // Unsigned Byte [1] + + // Unsigned Short [2] + + // Int [4] + + // Long [8] + + // Float [4] + + // Double [8] + + public void writeString(String string) { + byte[] buf; + + if (string.length() > Short.MAX_VALUE) { + //FIXME нужно выдавать предупреждение в лог + buf = string.substring(0, Short.MAX_VALUE).getBytes(StandardCharsets.UTF_8); + writeVarInt(Short.MAX_VALUE); + } else { + buf = string.getBytes(StandardCharsets.UTF_8); + writeVarInt(string.length()); + } + + writeBytes(buf); + } + + // Chat + + // Identifier + + public void writeVarInt(int value) { + while ((value & -128) != 0) { + writeByte(value & 127 | 128); + value >>>= 7; + } + + writeByte(value); + } + + // VarLong + + // Entity Metadata + + // Slot + + // NBT Tag + + // Position [8] + + // Angle [1] + + // UUID [16] + + public void writeBytes(byte[] buffer) { + writeBytes(buffer, 0, buffer.length); + } + + @Override + public void write(byte[] buffer) throws IOException { + writeBytes(buffer); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + writeBytes(buffer, offset, length); + } + + public abstract void writeByte(int value); + public abstract void writeBytes(byte[] buffer, int offset, int lengtn); + public abstract void writeShort(int value); +} diff --git a/src/main/java/mc/protocol/Packet.java b/src/main/java/mc/protocol/Packet.java new file mode 100644 index 0000000..f1ee9d7 --- /dev/null +++ b/src/main/java/mc/protocol/Packet.java @@ -0,0 +1,8 @@ +package mc.protocol; + +public interface Packet { + + void readSelf(NetInputStream netInputStream); + + void writeSelf(NetOutputStream netOutputStream); +} diff --git a/src/main/java/mc/protocol/PacketDirection.java b/src/main/java/mc/protocol/PacketDirection.java new file mode 100644 index 0000000..59f8231 --- /dev/null +++ b/src/main/java/mc/protocol/PacketDirection.java @@ -0,0 +1,6 @@ +package mc.protocol; + +public enum PacketDirection { + + SERVER_BOUND, CLIENT_BOUND +} diff --git a/src/main/java/mc/protocol/State.java b/src/main/java/mc/protocol/State.java new file mode 100644 index 0000000..bce5511 --- /dev/null +++ b/src/main/java/mc/protocol/State.java @@ -0,0 +1,67 @@ +package mc.protocol; + +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import mc.protocol.handshake.client.HandshakePacket; +import mc.protocol.status.client.StatusServerRequest; +import mc.protocol.status.server.StatusServerResponse; + +@RequiredArgsConstructor +public enum State { + + HANDSHAKING(-1){{ + setServerBoundPackets(ImmutableBiMap.of( + 0x00, HandshakePacket.class + )); + }}, + PLAY(0), + STATUS(1){{ + setServerBoundPackets(ImmutableBiMap.of( + 0x00, StatusServerRequest.class + )); + setClientBoundPackets(ImmutableBiMap.of( + 0x00, StatusServerResponse.class + )); + }}, + LOGIN(2); + + public static State getById(int id) { + for (State state : State.values()) { + if (state.id == id) { + return state; + } + } + + return null; + } + + @Getter + private final int id; + + @Setter(value = AccessLevel.PROTECTED) + private BiMap> clientBoundPackets; + + @Setter(value = AccessLevel.PROTECTED) + private BiMap> serverBoundPackets; + + public Class getPacketById(PacketDirection direction, int id) { + if (direction == PacketDirection.CLIENT_BOUND) { + return clientBoundPackets == null ? null : clientBoundPackets.get(id); + } else { + return serverBoundPackets == null ? null : serverBoundPackets.get(id); + } + } + + public Integer getIdByPacket(PacketDirection direction, Class clazz) { + if (direction == PacketDirection.CLIENT_BOUND) { + return clientBoundPackets == null ? null : clientBoundPackets.inverse().get(clazz); + } else { + return serverBoundPackets == null ? null : serverBoundPackets.inverse().get(clazz); + } + } +} + diff --git a/src/main/java/mc/protocol/coder/ByteArrayNetOutputStream.java b/src/main/java/mc/protocol/coder/ByteArrayNetOutputStream.java new file mode 100644 index 0000000..9329ffa --- /dev/null +++ b/src/main/java/mc/protocol/coder/ByteArrayNetOutputStream.java @@ -0,0 +1,34 @@ +package mc.protocol.coder; + +import mc.protocol.NetOutputStream; + +import java.io.ByteArrayOutputStream; + +class ByteArrayNetOutputStream extends NetOutputStream { + + private ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + @Override + public void writeByte(int value) { + baos.write(value); + } + + @Override + public void writeBytes(byte[] buffer, int offset, int lengtn) { + baos.write(buffer, offset, lengtn); + } + + @Override + public void writeShort(int value) { + baos.write((value >>> 8) & 0xFF); + baos.write(value & 0xFF); + } + + int size() { + return baos.size(); + } + + byte[] toByteArray() { + return baos.toByteArray(); + } +} diff --git a/src/main/java/mc/protocol/coder/ProtocolDecoder.java b/src/main/java/mc/protocol/coder/ProtocolDecoder.java new file mode 100644 index 0000000..9715c7b --- /dev/null +++ b/src/main/java/mc/protocol/coder/ProtocolDecoder.java @@ -0,0 +1,45 @@ +package mc.protocol.coder; + +/* +Packet format: + +| FIELD | TYPE | NOTES | ++------------+--------+-----------------------------------+ +| SIZE | VarInt | = sizeOf(id) + sizeOf(byte_array) | +| PACKET ID | VarInt | | +| BYTE ARRAY | bytes | | + +https://wiki.vg/index.php?title=Protocol&oldid=7368#Without_compression +*/ + +import lombok.RequiredArgsConstructor; +import mc.protocol.NetInputStream; +import mc.protocol.Packet; +import mc.protocol.PacketDirection; +import mc.protocol.State; + +import java.util.Objects; + +@RequiredArgsConstructor +public class ProtocolDecoder { + + private final PacketDirection direction; + + public Packet decode(State state, NetInputStream netInputStream) { + //TODO необходим механизм пропуска необработанных байтов + int sizePacket = netInputStream.readVarInt(); + + int packetId = netInputStream.readVarInt(); + Class packetClass = state.getPacketById(direction, packetId); + Objects.requireNonNull(packetClass); + + try { + Packet packet = packetClass.newInstance(); + packet.readSelf(netInputStream); + return packet; + } catch (InstantiationException | IllegalAccessException e) { + e.printStackTrace(); //FIXME нужно писать в лог + return null; + } + } +} diff --git a/src/main/java/mc/protocol/coder/ProtocolEncoder.java b/src/main/java/mc/protocol/coder/ProtocolEncoder.java new file mode 100644 index 0000000..a30170f --- /dev/null +++ b/src/main/java/mc/protocol/coder/ProtocolEncoder.java @@ -0,0 +1,37 @@ +package mc.protocol.coder; + +import lombok.RequiredArgsConstructor; +import mc.protocol.*; + +import java.util.Objects; + +/* +Packet format: + +| FIELD | TYPE | NOTES | ++------------+--------+-----------------------------------+ +| SIZE | VarInt | = sizeOf(id) + sizeOf(byte_array) | +| PACKET ID | VarInt | | +| BYTE ARRAY | bytes | | + +https://wiki.vg/index.php?title=Protocol&oldid=7368#Without_compression +*/ + +@RequiredArgsConstructor +public class ProtocolEncoder { + + private final PacketDirection direction; + + public void encode(State state, Packet packet, NetOutputStream netOutputStream) { + Integer packetId = state.getIdByPacket(direction, packet.getClass()); + Objects.requireNonNull(packetId); + + ByteArrayNetOutputStream banos = new ByteArrayNetOutputStream(); + banos.writeVarInt(packetId); + packet.writeSelf(banos); + + netOutputStream.writeVarInt(banos.size()); + netOutputStream.writeBytes(banos.toByteArray()); + } + +} diff --git a/src/main/java/mc/protocol/handshake/client/HandshakePacket.java b/src/main/java/mc/protocol/handshake/client/HandshakePacket.java new file mode 100644 index 0000000..2d2f148 --- /dev/null +++ b/src/main/java/mc/protocol/handshake/client/HandshakePacket.java @@ -0,0 +1,32 @@ +package mc.protocol.handshake.client; + +import lombok.Data; +import mc.protocol.NetInputStream; +import mc.protocol.NetOutputStream; +import mc.protocol.Packet; +import mc.protocol.State; + +@Data +public class HandshakePacket implements Packet { + + private int protocolVersion; + private String ip; + private int port; + private State nextState; + + @Override + public void readSelf(NetInputStream netInputStream) { + protocolVersion = netInputStream.readVarInt(); + ip = netInputStream.readString(255); + port = netInputStream.readUnsignedShort(); + nextState = State.getById(netInputStream.readVarInt()); + } + + @Override + public void writeSelf(NetOutputStream netOutputStream) { + netOutputStream.writeVarInt(protocolVersion); + netOutputStream.writeString(ip); + netOutputStream.writeShort(port); + netOutputStream.writeVarInt(nextState.getId()); + } +} diff --git a/src/main/java/mc/protocol/status/client/StatusServerRequest.java b/src/main/java/mc/protocol/status/client/StatusServerRequest.java new file mode 100644 index 0000000..4bf640a --- /dev/null +++ b/src/main/java/mc/protocol/status/client/StatusServerRequest.java @@ -0,0 +1,18 @@ +package mc.protocol.status.client; + +import mc.protocol.NetInputStream; +import mc.protocol.NetOutputStream; +import mc.protocol.Packet; + +public class StatusServerRequest implements Packet { + + @Override + public void readSelf(NetInputStream netInputStream) { + // empty + } + + @Override + public void writeSelf(NetOutputStream netOutputStream) { + // empty + } +} diff --git a/src/main/java/mc/protocol/status/server/StatusServerResponse.java b/src/main/java/mc/protocol/status/server/StatusServerResponse.java new file mode 100644 index 0000000..ea78013 --- /dev/null +++ b/src/main/java/mc/protocol/status/server/StatusServerResponse.java @@ -0,0 +1,22 @@ +package mc.protocol.status.server; + +import lombok.Data; +import mc.protocol.NetInputStream; +import mc.protocol.NetOutputStream; +import mc.protocol.Packet; + +@Data +public class StatusServerResponse implements Packet { + + private String info; + + @Override + public void readSelf(NetInputStream netInputStream) { + info = netInputStream.readString(); + } + + @Override + public void writeSelf(NetOutputStream netOutputStream) { + netOutputStream.writeString(info); + } +}