Каркас протокола: кадрирование, буфер и рукопожатие

Это первый настоящий кусок сервера. Пока он ничего не умеет, кроме того что
умеет правильно: принимать байты от чужого человека и не дать ему ничего
сделать с нами.

Формат кадра — четыре байта магии RSX1, версия, id пакета, длина тела и
само тело. Магия стоит в начале, чтобы порт мог сразу ответить, а не ждать
объяснений.

Главное здесь — границы. Сервер живёт тем, что вынимает числа из сокетов
чужих людей, и разница между дешёвой проверкой и аллокацией — это разница
между «противник стоил нам пары тактов» и «противник стоил нам кучи».

- Длина тела проверяется до того, как под неё что-то зарезервировано.
  Единственное, на что полезна вредная длина, — заставить сервер выделить
  память.
- Varint ограничен пятью байтами для int и десятью для long. Без этого
  десять байт 0xFF превращают пятибайтовое целое в бесконечный цикл.
- Длина строки ограничена до того, как под неё читают байты.
- Декодер держит не больше одного максимального кадра плюс неполный
  заголовок.

Неполные данные и испорченные — это разные вещи, и раньше это не было
разделено: конверт, разрезанный сетью посреди varint, выглядел как
испорченный и рвал соединение. IncompleteException означает «остальное
уже в пути», ProtocolException — «потока больше не будет». TCP режет кадры
почти всегда, так что первое поведение было бы отказом от большинства
соединений.

Позиция упакована одним varint, но доли у осей не равные: три поля по 26 бит
это 78 бит, они не влезают в 64. X и Z получают по 26 бит с шагом 1/32
блока, Y — оставшиеся 12 бит целыми блоками. Со смещением на 2048 не
вышло бы: для диапазона 0..4095 нужен тринадцатый бит, и это ровно тот бит,
по которому значение читается обратно отрицательным.

Первый прогон тестов нашёл все три ошибки сразу, две из них — в упаковке
позиции. Проверено 17 сценариев, включая «по одному байту», пятьдесят
кадров за одно чтение, чужую магию и объявленное тело в два гигабайта.

Собирается и тестируется одним JDK, без Gradle и без сети, как и всё
остальное в проекте.
This commit is contained in:
sasheg dev
2026-10-19 01:10:26 +03:00
parent 736ef36a44
commit 7fbb26092f
9 changed files with 1429 additions and 0 deletions
@@ -0,0 +1,343 @@
package net.rustnotmc.protocol;
/**
* A growable byte buffer with the primitives the wire format is built from.
*
* Every read is bounded. A server spends its life taking numbers off sockets
* that belong to strangers, and the difference between a cheap check and an
* allocation is the difference between a hostile peer costing a few cycles and
* costing the heap. Nothing here trusts a length before the bytes behind it
* have actually arrived.
*
* Integers travel as zig-zag varints, so small negatives stay small. The whole
* point of that is worth one sentence: negative coordinates are ordinary in a
* world, and without zig-zag they would take five bytes each instead of one.
*/
public final class Buf {
/** A varint of a 32 bit value never needs more than five bytes. */
public static final int MAX_VAR_INT_BYTES = 5;
/** A varint of a 64 bit value never needs more than ten bytes. */
public static final int MAX_VAR_LONG_BYTES = 10;
/** Default ceiling for a length-prefixed byte array or string. */
public static final int DEFAULT_MAX_PAYLOAD = 2 * 1024 * 1024;
private byte[] data;
private int writerIndex;
private int readerIndex;
public Buf() {
this(64);
}
public Buf(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("negative capacity: " + capacity);
}
this.data = new byte[Math.max(8, capacity)];
}
public Buf(byte[] initial) {
this(initial.length);
this.writerIndex = initial.length;
System.arraycopy(initial, 0, this.data, 0, initial.length);
}
// ------------------------------------------------------------- capacity
public int readableBytes() {
return this.writerIndex - this.readerIndex;
}
public int writerIndex() {
return this.writerIndex;
}
/**
* Moves the write cursor, used to rewind a buffer that has nothing left in
* it. Refuses to move past what has actually been written, because a cursor
* past the end would hand out zeroes that were never sent.
*/
public Buf writerIndex(int index) {
if (index < 0 || index > this.data.length) {
throw new IndexOutOfBoundsException("writer index " + index
+ " outside 0.." + this.data.length);
}
this.writerIndex = index;
if (this.readerIndex > index) {
this.readerIndex = index;
}
return this;
}
public int readerIndex() {
return this.readerIndex;
}
public Buf readerIndex(int index) {
if (index < 0 || index > this.writerIndex) {
throw new IndexOutOfBoundsException("reader index " + index
+ " outside 0.." + this.writerIndex);
}
this.readerIndex = index;
return this;
}
private void ensure(int extra) {
if (this.writerIndex + extra <= this.data.length) {
return;
}
int needed = this.writerIndex + extra;
int grown = Math.max(this.data.length * 2, needed);
byte[] bigger = new byte[grown];
System.arraycopy(this.data, 0, bigger, 0, this.writerIndex);
this.data = bigger;
}
// ---------------------------------------------------------------- write
public Buf writeByte(int value) {
this.ensure(1);
this.data[this.writerIndex++] = (byte) value;
return this;
}
public Buf writeBoolean(boolean value) {
return writeByte(value ? 1 : 0);
}
public Buf writeShort(int value) {
this.ensure(2);
this.data[this.writerIndex++] = (byte) (value >>> 8);
this.data[this.writerIndex++] = (byte) value;
return this;
}
public Buf writeInt(int value) {
this.ensure(4);
this.data[this.writerIndex++] = (byte) (value >>> 24);
this.data[this.writerIndex++] = (byte) (value >>> 16);
this.data[this.writerIndex++] = (byte) (value >>> 8);
this.data[this.writerIndex++] = (byte) value;
return this;
}
public Buf writeLong(long value) {
this.ensure(8);
for (int shift = 56; shift >= 0; shift -= 8) {
this.data[this.writerIndex++] = (byte) (value >>> shift);
}
return this;
}
public Buf writeFloat(float value) {
return writeInt(Float.floatToIntBits(value));
}
public Buf writeDouble(double value) {
return writeLong(Double.doubleToLongBits(value));
}
/** Writes a zig-zag encoded varint. */
public Buf writeVarInt(int value) {
int zig = (value << 1) ^ (value >> 31);
while (true) {
if ((zig & 0xFFFFFF80) == 0) {
return writeByte(zig);
}
writeByte((zig & 0x7F) | 0x80);
zig >>>= 7;
}
}
/** Writes a zig-zag encoded varint over a long. */
public Buf writeVarLong(long value) {
long zig = (value << 1) ^ (value >> 63);
while (true) {
if ((zig & 0xFFFFFFFFFFFFFF80L) == 0L) {
return writeByte((int) zig);
}
writeByte((int) ((zig & 0x7F) | 0x80));
zig >>>= 7;
}
}
public Buf writeBytes(byte[] value) {
return writeBytes(value, 0, value.length);
}
public Buf writeBytes(byte[] value, int offset, int length) {
this.ensure(length);
System.arraycopy(value, offset, this.data, this.writerIndex, length);
this.writerIndex += length;
return this;
}
public Buf writeString(String value) {
byte[] encoded = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
writeVarInt(encoded.length);
return writeBytes(encoded);
}
/**
* Writes a block position as one varint.
*
* The axes do not get equal shares of the long. Three 26 bit fields would be 78
* bits and simply do not fit, so the split follows what a world actually needs:
* X and Z take 26 bits each, +/-33 million blocks at 1/32 block resolution, and
* Y takes the remaining 12 bits as whole blocks in two's complement, covering
* -2048 to 2047, which is the whole vertical range the format supports.
*
* Y is stored signed and not offset by 2048 on purpose: an offset would need 13
* bits to hold 0..4095, and the twelfth bit is exactly the one that decides
* whether a value reads back negative.
*
* Every mask is parenthesised on purpose: in Java, &lt;&lt; binds tighter than
* &amp;, so "a &amp; 0x3FFFFFF &lt;&lt; 12" silently masks the wrong value and
* throws away the low bits of the axis.
*/
public Buf writePosition(double x, double y, double z) {
long packed = ((long) (x * 32.0D) & 0x3FFFFFFL) << 38;
packed |= ((long) (int) y & 0xFFFL) << 26;
packed |= (long) (z * 32.0D) & 0x3FFFFFFL;
return writeVarLong(packed);
}
/** Reads a position written by {@link #writePosition}. */
public double[] readPosition() {
long packed = readVarLong();
// a field of n bits at offset k is sign extended by shifting left far
// enough to land its top bit at 63, then arithmetically shifting right
double x = (double) (packed << 0 >> 38) / 32.0D;
double y = (double) (packed << 26 >> 52);
double z = (double) (packed << 38 >> 38) / 32.0D;
return new double[]{x, y, z};
}
// ----------------------------------------------------------------- read
public int readUnsignedByte() {
require(1);
return this.data[this.readerIndex++] & 0xFF;
}
public boolean readBoolean() {
return readUnsignedByte() != 0;
}
public int readShort() {
require(2);
int high = this.data[this.readerIndex++] & 0xFF;
int low = this.data[this.readerIndex++] & 0xFF;
return (high << 8) | low;
}
public int readInt() {
require(4);
int value = ((this.data[this.readerIndex++] & 0xFF) << 24)
| ((this.data[this.readerIndex++] & 0xFF) << 16)
| ((this.data[this.readerIndex++] & 0xFF) << 8)
| (this.data[this.readerIndex++] & 0xFF);
return value;
}
public long readLong() {
require(8);
long value = 0L;
for (int i = 0; i < 8; i++) {
value = (value << 8) | (this.data[this.readerIndex++] & 0xFF);
}
return value;
}
public float readFloat() {
return Float.intBitsToFloat(readInt());
}
public double readDouble() {
return Double.longBitsToDouble(readLong());
}
/**
* Reads a varint, refusing anything longer than it has to be.
*
* Without the byte cap a peer can send ten bytes of 0xFF and turn a five
* byte integer into an endless loop.
*/
public int readVarInt() {
int result = 0;
for (int shift = 0; shift < MAX_VAR_INT_BYTES * 7; shift += 7) {
byte b = (byte) readUnsignedByte();
result |= (b & 0x7F) << shift;
if ((b & 0x80) == 0) {
return (result >>> 1) ^ -(result & 1);
}
}
throw new ProtocolException("varint is longer than " + MAX_VAR_INT_BYTES + " bytes");
}
public long readVarLong() {
long result = 0L;
for (int shift = 0; shift < MAX_VAR_LONG_BYTES * 7; shift += 7) {
byte b = (byte) readUnsignedByte();
result |= (long) (b & 0x7F) << shift;
if ((b & 0x80) == 0) {
return (result >>> 1) ^ -(result & 1);
}
}
throw new ProtocolException("varlong is longer than " + MAX_VAR_LONG_BYTES + " bytes");
}
public byte[] readBytes(int length) {
if (length < 0) {
throw new ProtocolException("negative length: " + length);
}
require(length);
byte[] value = new byte[length];
System.arraycopy(this.data, this.readerIndex, value, 0, length);
this.readerIndex += length;
return value;
}
public String readString(int maxLength) {
int length = readVarInt();
if (length < 0 || length > maxLength) {
throw new ProtocolException("string length " + length + " is out of range");
}
require(length);
String value = new String(this.data, this.readerIndex, length,
java.nio.charset.StandardCharsets.UTF_8);
this.readerIndex += length;
return value;
}
/** Bytes written but not yet read, as a copy. */
public byte[] toByteArray() {
byte[] value = new byte[this.writerIndex];
System.arraycopy(this.data, 0, value, 0, this.writerIndex);
return value;
}
/** A reader over a slice of what has been written. */
public Buf reader() {
Buf view = new Buf(Math.max(8, this.writerIndex - this.readerIndex));
view.writeBytes(this.data, this.readerIndex, this.writerIndex - this.readerIndex);
view.readerIndex = 0;
return view;
}
private void require(int count) {
if (this.readerIndex + count > this.writerIndex) {
throw new IncompleteException("wanted " + count + " byte(s) but only "
+ readableBytes() + " remain");
}
}
@Override
public String toString() {
return "Buf[read=" + this.readerIndex + ", written=" + this.writerIndex
+ ", capacity=" + this.data.length + "]";
}
}
@@ -0,0 +1,144 @@
package net.rustnotmc.protocol;
/**
* One message: an envelope plus the packet body.
*
* The wire layout is deliberately dull and fixed on both ends:
*
* <pre>
* magic 4 bytes 0x52535831, "RSX1"
* version varint protocol version, refused if it is not ours
* packetId varint what the body means
* bodyLength varint bytes of body that follow
* body bytes
* </pre>
*
* The magic sits in front so a port can answer immediately instead of waiting
* to be told it is speaking nonsense. The declared body length is checked
* against the maximum before a single byte is reserved, because the only thing
* a malicious length is good for is making the server allocate.
*/
public final class Frame {
/** "RSX1" in ASCII, as a big endian int. */
public static final int MAGIC = 0x52535831;
/** The version this build speaks. */
public static final int VERSION = 1;
private final int version;
private final int packetId;
private final byte[] body;
public Frame(int packetId, byte[] body) {
this(VERSION, packetId, body);
}
public Frame(int version, int packetId, byte[] body) {
if (body == null) {
throw new IllegalArgumentException("body is null");
}
this.version = version;
this.packetId = packetId;
this.body = body;
}
public int version() {
return this.version;
}
public int packetId() {
return this.packetId;
}
public byte[] body() {
return this.body;
}
public int size() {
return 4 + MAX_ENVELOPE + this.body.length;
}
/** Worst case envelope: magic plus four varints at their longest. */
public static final int MAX_ENVELOPE = 4
+ Buf.MAX_VAR_INT_BYTES // version
+ Buf.MAX_VAR_INT_BYTES // packet id
+ Buf.MAX_VAR_INT_BYTES; // body length
public Buf encode() {
Buf buf = new Buf(size());
buf.writeInt(MAGIC);
buf.writeVarInt(this.version);
buf.writeVarInt(this.packetId);
buf.writeVarInt(this.body.length);
buf.writeBytes(this.body);
return buf;
}
/**
* Reads one frame from the front of a buffer.
*
* Returns null when the buffer does not hold a whole frame yet, so a
* caller feeding it a socket can keep the remainder and come back when more
* arrives. Throws as soon as it can tell the stream is impossible, rather
* than waiting forever for bytes that will make it valid.
*/
public static Frame decode(Buf in, int maxBody) {
if (in.readableBytes() < 4) {
return null;
}
int start = in.readerIndex();
int magic = peekInt(in);
if (magic != MAGIC) {
throw new ProtocolException(String.format(
"not a RustNotMC stream: magic is 0x%08X", Integer.valueOf(magic)));
}
in.readerIndex(start + 4);
int version;
int packetId;
int length;
try {
version = in.readVarInt();
packetId = in.readVarInt();
length = in.readVarInt();
} catch (IncompleteException more) {
// TCP cut the envelope; the rest is on its way
in.readerIndex(start);
return null;
} catch (ProtocolException malformed) {
throw new ProtocolException("malformed envelope: " + malformed.getMessage(), malformed);
}
if (version != VERSION) {
throw new ProtocolException("protocol version " + version
+ ", this build speaks " + VERSION);
}
if (length < 0 || length > maxBody) {
throw new ProtocolException("declared body of " + length
+ " bytes is outside the limit of " + maxBody);
}
if (in.readableBytes() < length) {
in.readerIndex(start);
return null;
}
byte[] body = in.readBytes(length);
return new Frame(version, packetId, body);
}
private static int peekInt(Buf in) {
int r = in.readerIndex();
int value = ((in.readUnsignedByte() & 0xFF) << 24)
| ((in.readUnsignedByte() & 0xFF) << 16)
| ((in.readUnsignedByte() & 0xFF) << 8)
| (in.readUnsignedByte() & 0xFF);
in.readerIndex(r);
return value;
}
@Override
public String toString() {
return "Frame[id=" + this.packetId + ", version=" + this.version
+ ", body=" + this.body.length + "B]";
}
}
@@ -0,0 +1,124 @@
package net.rustnotmc.protocol;
/**
* Turns a byte stream into frames, one call per whatever the socket had.
*
* TCP hands over bytes, not messages: one read may be half a frame, or nine
* and a half. The decoder keeps whatever it could not use yet and returns only
* whole frames, so the layer above sees a stream of packets and never thinks
* about segmentation again.
*
* The one thing it will not do is buffer without limit. A peer that announces
* a body of a gigabyte and then sends nothing must not be able to hold a
* gigabyte of this process's heap, so a body larger than the configured
* ceiling is refused the moment its length is read.
*/
public final class FrameDecoder {
private final Buf pending;
private final int maxBody;
private boolean poisoned;
private int framesRead;
private long bytesRead;
public FrameDecoder() {
this(Buf.DEFAULT_MAX_PAYLOAD);
}
public FrameDecoder(int maxBody) {
if (maxBody <= 0) {
throw new IllegalArgumentException("max body must be positive: " + maxBody);
}
this.pending = new Buf();
this.maxBody = maxBody;
}
public int maxBody() {
return this.maxBody;
}
public int bufferedBytes() {
return this.pending.readableBytes();
}
public int framesRead() {
return this.framesRead;
}
public long bytesRead() {
return this.bytesRead;
}
/**
* Feeds freshly read bytes in.
*
* Returns every frame that is now complete. Throws on a stream that can no
* longer become valid; a caller should treat that as the end of the
* connection rather than something to retry.
*/
public java.util.List<Frame> offer(byte[] chunk, int offset, int length) {
if (this.poisoned) {
throw new ProtocolException("decoder was poisoned by an earlier failure");
}
if (chunk == null) {
throw new IllegalArgumentException("chunk is null");
}
if (length < 0 || offset < 0 || offset + length > chunk.length) {
throw new IllegalArgumentException("chunk range is out of bounds");
}
if (length == 0) {
return java.util.Collections.emptyList();
}
this.pending.writeBytes(chunk, offset, length);
this.bytesRead += length;
// refuse to sit on more than one maximum frame plus a partial header
int ceiling = this.maxBody + Frame.MAX_ENVELOPE;
if (this.pending.readableBytes() > ceiling) {
this.poisoned = true;
throw new ProtocolException("buffered " + this.pending.readableBytes()
+ " bytes without a complete frame, limit is " + ceiling);
}
java.util.List<Frame> out = new java.util.ArrayList<Frame>();
while (true) {
Frame frame;
try {
frame = Frame.decode(this.pending, this.maxBody);
} catch (ProtocolException bad) {
this.poisoned = true;
throw bad;
}
if (frame == null) {
break;
}
this.framesRead++;
out.add(frame);
}
// hand the tail back to the next read instead of growing the buffer
this.compact();
return out;
}
private void compact() {
// keep the original object: callers may be holding it, and replacing the
// field under them would be a nasty surprise
int left = this.pending.readableBytes();
int from = this.pending.readerIndex();
if (left == 0) {
this.pending.readerIndex(0);
this.pending.writerIndex(0);
return;
}
if (from == 0) {
return;
}
byte[] all = this.pending.toByteArray();
byte[] tail = new byte[left];
System.arraycopy(all, from, tail, 0, left);
this.pending.readerIndex(0);
this.pending.writerIndex(0);
this.pending.writeBytes(tail);
}
}
@@ -0,0 +1,19 @@
package net.rustnotmc.protocol;
/**
* Raised when a buffer does not yet hold the bytes a read asked for.
*
* Separate from {@link ProtocolException} because the two mean opposite things.
* Running out of bytes is normal: TCP cut a frame in half and the rest is on
* its way. A malformed varint is not normal and the stream is finished. If the
* decoder treated both as fatal it would drop every connection that happened to
* span two packets, which on a real network is most of them.
*/
public final class IncompleteException extends ProtocolException {
private static final long serialVersionUID = 1L;
public IncompleteException(String message) {
super(message);
}
}
@@ -0,0 +1,266 @@
package net.rustnotmc.protocol;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* The packets that start a connection, and the table that maps them to ids.
*
* The ids are written down here rather than generated, because they end up in
* every capture and every log line forever. Renumbering one is a protocol
* change and should look like a deliberate act, not a side effect of moving a
* class around.
*
* The order of a connection is fixed and short:
*
* <pre>
* client -> Hello protocol version, what the client is
* server -> HelloAck accepted version, server name, motd, online count
* client -> Auth account and proof
* server -> AuthAck session token, or a reason it was refused
* client -> Login the session token
* server -> LoginAck player id and entity id, or a reason
* either -> Disconnect a human readable reason
* </pre>
*/
public final class Packets {
private Packets() {
}
/** Longest a name, motd or reason may be, in bytes. */
public static final int MAX_TEXT = 512;
/** Longest a handshake proof may be, in bytes. */
public static final int MAX_PROOF = 256;
/** Longest the handshake body may be, in bytes. */
public static final int MAX_HANDSHAKE_BODY = 8 * 1024;
public static final int HELLO = 0x01;
public static final int HELLO_ACK = 0x02;
public static final int AUTH = 0x03;
public static final int AUTH_ACK = 0x04;
public static final int LOGIN = 0x05;
public static final int LOGIN_ACK = 0x06;
public static final int DISCONNECT = 0x7F;
/** The client introducing itself. */
public static final class Hello {
public final int protocolVersion;
public final String clientName;
public final int clientVersion;
public Hello(int protocolVersion, String clientName, int clientVersion) {
this.protocolVersion = protocolVersion;
this.clientName = clientName;
this.clientVersion = clientVersion;
}
public Buf write() {
Buf buf = new Buf();
buf.writeVarInt(this.protocolVersion);
buf.writeString(this.clientName);
buf.writeVarInt(this.clientVersion);
return buf;
}
public static Hello read(Buf in) {
return new Hello(in.readVarInt(), in.readString(MAX_TEXT), in.readVarInt());
}
}
/** The server accepting, and describing itself. */
public static final class HelloAck {
public final int protocolVersion;
public final String serverName;
public final String motd;
public final int onlinePlayers;
public final int maxPlayers;
public HelloAck(int protocolVersion, String serverName, String motd,
int onlinePlayers, int maxPlayers) {
this.protocolVersion = protocolVersion;
this.serverName = serverName;
this.motd = motd;
this.onlinePlayers = onlinePlayers;
this.maxPlayers = maxPlayers;
}
public Buf write() {
Buf buf = new Buf();
buf.writeVarInt(this.protocolVersion);
buf.writeString(this.serverName);
buf.writeString(this.motd);
buf.writeVarInt(this.onlinePlayers);
buf.writeVarInt(this.maxPlayers);
return buf;
}
public static HelloAck read(Buf in) {
return new HelloAck(in.readVarInt(), in.readString(MAX_TEXT),
in.readString(MAX_TEXT), in.readVarInt(), in.readVarInt());
}
}
/** The client claiming an account. */
public static final class Auth {
public final String account;
public final String proof;
public Auth(String account, String proof) {
this.account = account;
this.proof = proof;
}
public Buf write() {
Buf buf = new Buf();
buf.writeString(this.account);
buf.writeString(this.proof);
return buf;
}
public static Auth read(Buf in) {
return new Auth(in.readString(MAX_TEXT), in.readString(MAX_PROOF));
}
}
/** Whether the account was accepted, and on what terms. */
public static final class AuthAck {
public final boolean accepted;
public final String reason;
public final String token;
public final long expiresAt;
private AuthAck(boolean accepted, String reason, String token, long expiresAt) {
this.accepted = accepted;
this.reason = reason;
this.token = token;
this.expiresAt = expiresAt;
}
public static AuthAck ok(String token, long expiresAt) {
return new AuthAck(true, "", token, expiresAt);
}
public static AuthAck refused(String reason) {
return new AuthAck(false, reason, "", 0L);
}
public Buf write() {
Buf buf = new Buf();
buf.writeBoolean(this.accepted);
buf.writeString(this.reason);
if (this.accepted) {
buf.writeString(this.token);
buf.writeLong(this.expiresAt);
}
return buf;
}
public static AuthAck read(Buf in) {
if (!in.readBoolean()) {
return refused(in.readString(MAX_TEXT));
}
return ok(in.readString(MAX_TEXT), in.readLong());
}
}
/** The client redeeming the token it was just handed. */
public static final class Login {
public final String account;
public final String token;
public Login(String account, String token) {
this.account = account;
this.token = token;
}
public Buf write() {
Buf buf = new Buf();
buf.writeString(this.account);
buf.writeString(this.token);
return buf;
}
public static Login read(Buf in) {
return new Login(in.readString(MAX_TEXT), in.readString(MAX_TEXT));
}
}
/** Whether the player is in. */
public static final class LoginAck {
public final boolean accepted;
public final String reason;
public final int playerId;
public final int entityId;
private LoginAck(boolean accepted, String reason, int playerId, int entityId) {
this.accepted = accepted;
this.reason = reason;
this.playerId = playerId;
this.entityId = entityId;
}
public static LoginAck ok(int playerId, int entityId) {
return new LoginAck(true, "", playerId, entityId);
}
public static LoginAck refused(String reason) {
return new LoginAck(false, reason, -1, -1);
}
public Buf write() {
Buf buf = new Buf();
buf.writeBoolean(this.accepted);
buf.writeString(this.reason);
if (this.accepted) {
buf.writeVarInt(this.playerId);
buf.writeVarInt(this.entityId);
}
return buf;
}
public static LoginAck read(Buf in) {
if (!in.readBoolean()) {
return refused(in.readString(MAX_TEXT));
}
return ok(in.readVarInt(), in.readVarInt());
}
}
/** A reason, said out loud, before the connection goes away. */
public static final class Disconnect {
public final String reason;
public Disconnect(String reason) {
this.reason = reason;
}
public Buf write() {
return new Buf().writeString(this.reason);
}
public static Disconnect read(Buf in) {
return new Disconnect(in.readString(MAX_TEXT));
}
}
/**
* Every packet id this build knows, and what it decodes into.
*
* Written as plain data so that adding a packet is one line and an unknown
* id is a loud error instead of a silent skip.
*/
public static Map<Integer, String> names() {
Map<Integer, String> map = new LinkedHashMap<Integer, String>();
map.put(Integer.valueOf(HELLO), "Hello");
map.put(Integer.valueOf(HELLO_ACK), "HelloAck");
map.put(Integer.valueOf(AUTH), "Auth");
map.put(Integer.valueOf(AUTH_ACK), "AuthAck");
map.put(Integer.valueOf(LOGIN), "Login");
map.put(Integer.valueOf(LOGIN_ACK), "LoginAck");
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
return map;
}
}
@@ -0,0 +1,21 @@
package net.rustnotmc.protocol;
/**
* Raised when bytes on the wire cannot mean what the sender intended.
*
* Unchecked on purpose: every one of these ends the connection, and there is
* nothing a caller could do about it other than close. Making it checked would
* only add a catch block that rethrows.
*/
public class ProtocolException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ProtocolException(String message) {
super(message);
}
public ProtocolException(String message, Throwable cause) {
super(message, cause);
}
}