b5fb57bb4f
- Новый пакет LOD_REGION: сервер отдаёт участок дальнего вида — сетку высот и цветов, по одной пробе каждые 4 блока. - LodGenerator на сервере считает участки от начала мира, держит их в памяти и пишет на диск рядом с миром, чтобы считать один раз. - Клиент строит из участка сетку и рисует своим шейдером (#version 330) до обычных чанков: чанки ближе и подробнее, поэтому закрывают дальний вид. - Дальний вид приходит по мере того, как игрок ходит по миру: сервер шлёт участки вокруг него раз в тик. Проверки в сервере 96, в протоколе 24, в клиенте 128.
1954 lines
64 KiB
Java
1954 lines
64 KiB
Java
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>
|
|
*
|
|
* After that the play phase begins, and it has no fixed order:
|
|
*
|
|
* <pre>
|
|
* client -> JoinWorld ask to be put in the world
|
|
* server -> WorldJoin the world, the spawn, the view distance
|
|
* server -> ChunkData a chunk, as it becomes visible
|
|
* either -> BlockChange one block, changed
|
|
* client -> PlayerMove where the player is
|
|
* server -> EntityMove where somebody else is
|
|
* either -> Ping/Pong is anybody there
|
|
* </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;
|
|
|
|
/** A salt and a nonce, in the sizes the crypto actually produces. */
|
|
public static final int SALT_BYTES = 16;
|
|
public static final int CHALLENGE_BYTES = 32;
|
|
|
|
/** Rounds of PBKDF2 that both ends must use. */
|
|
public static final int KEY_ITERATIONS = 210000;
|
|
|
|
/** Derived key length in bytes, matching PBKDF2WithHmacSHA256. */
|
|
public static final int KEY_BYTES = 32;
|
|
|
|
/** 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;
|
|
|
|
/**
|
|
* The play phase, once the handshake is done.
|
|
*
|
|
* Kept apart from the handshake ids so that a packet can never be mistaken
|
|
* for one it is not: the handshake happens exactly once, in a fixed order,
|
|
* and the play phase happens in any order, for as long as the player stays.
|
|
*/
|
|
public static final int JOIN_WORLD = 0x10;
|
|
public static final int WORLD_JOIN = 0x11;
|
|
public static final int CHUNK_DATA = 0x12;
|
|
|
|
/**
|
|
* One block changed.
|
|
*
|
|
* The same packet in both directions: the client asks for a block to
|
|
* change, and the server tells everybody who can see it that it did.
|
|
*/
|
|
public static final int BLOCK_CHANGE = 0x13;
|
|
|
|
public static final int PLAYER_MOVE = 0x14;
|
|
public static final int ENTITY_MOVE = 0x15;
|
|
public static final int PING = 0x16;
|
|
public static final int PONG = 0x17;
|
|
public static final int ENTITY_SPAWN = 0x18;
|
|
public static final int ENTITY_REMOVE = 0x19;
|
|
|
|
/**
|
|
* The launcher's session, presented to the game server.
|
|
*
|
|
* The launcher logs the player in once and hands the game a token. The
|
|
* game server asks the launcher whether that token is good, and the player
|
|
* is in. This is the part of the handshake that Yggdrasil does in
|
|
* vanilla, done with our own launcher instead of Mojang's.
|
|
*/
|
|
public static final int TOKEN_AUTH = 0x1A;
|
|
|
|
/**
|
|
* The whole inventory, sent when the player arrives.
|
|
*
|
|
* The server is the only one who decides what is in the slots, so the
|
|
* client is told rather than asked.
|
|
*/
|
|
public static final int INVENTORY = 0x1C;
|
|
|
|
/** One slot of the inventory, when it changes. */
|
|
public static final int INVENTORY_SLOT = 0x1D;
|
|
|
|
/** The client starts or stops digging. */
|
|
public static final int DIG = 0x1E;
|
|
|
|
/** How far the digging has come, for the cracks. */
|
|
public static final int BREAK_PROGRESS = 0x1F;
|
|
|
|
/** The client asks to make something. */
|
|
public static final int CRAFT_START = 0x20;
|
|
|
|
/** What the server says about a craft. */
|
|
public static final int CRAFT_UPDATE = 0x21;
|
|
|
|
/** The client asks to research an item at the research table. */
|
|
public static final int RESEARCH_START = 0x22;
|
|
|
|
/** What the server says about a research. */
|
|
public static final int RESEARCH_UPDATE = 0x23;
|
|
|
|
/** The client asks to open a node of the tree of a workbench. */
|
|
public static final int TECH_TREE_UNLOCK = 0x24;
|
|
|
|
/** The recipes the player knows. */
|
|
public static final int KNOWN_RECIPES = 0x25;
|
|
|
|
/** The nodes of the trees the player has opened. */
|
|
public static final int TECH_TREE_STATE = 0x26;
|
|
|
|
/** A container the player may look into. */
|
|
public static final int CONTAINER_OPEN = 0x27;
|
|
|
|
/** The client takes something out of a container. */
|
|
public static final int CONTAINER_TAKE = 0x28;
|
|
|
|
/** The client learns a blueprint. */
|
|
public static final int LEARN_BLUEPRINT = 0x29;
|
|
|
|
/** The boxes of every model the server knows. */
|
|
public static final int HITBOXES = 0x2A;
|
|
|
|
/** The places where those models stand. */
|
|
public static final int HITBOX_INSTANCES = 0x2B;
|
|
|
|
/** The client asks the server to read its box file again. */
|
|
public static final int HITBOXES_RELOAD = 0x2C;
|
|
|
|
/** A piece of the far view: the ground of one region, in a grid. */
|
|
public static final int LOD_REGION = 0x34;
|
|
|
|
/** The client hits something. */
|
|
public static final int ATTACK = 0x2D;
|
|
|
|
/** What somebody said. */
|
|
public static final int CHAT = 0x2E;
|
|
|
|
/** The client says something. */
|
|
public static final int CHAT_SEND = 0x2F;
|
|
|
|
/** Who is in the world. */
|
|
public static final int PLAYER_LIST = 0x30;
|
|
|
|
/** The client asks for a longer session. */
|
|
public static final int SESSION_RELOAD = 0x31;
|
|
|
|
/** The session was made longer. */
|
|
public static final int SESSION_RENEWED = 0x32;
|
|
|
|
/** Whether the player is one of the administrators. */
|
|
public static final int ADMIN_STATE = 0x33;
|
|
|
|
/**
|
|
* One slot of the inventory and what is in it now.
|
|
*
|
|
* The slot number is here because the client has to know which one to
|
|
* redraw, and the contents because it has no way of guessing them.
|
|
*/
|
|
public static final class SlotChange implements Packet {
|
|
|
|
public final int slot;
|
|
public final int item;
|
|
public final int count;
|
|
public final int condition;
|
|
|
|
public SlotChange(int slot, int item, int count, int condition) {
|
|
this.slot = slot;
|
|
this.item = item;
|
|
this.count = count;
|
|
this.condition = condition;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(24);
|
|
buf.writeVarInt(this.slot);
|
|
buf.writeVarInt(this.item);
|
|
buf.writeVarInt(this.count);
|
|
buf.writeVarInt(this.condition);
|
|
return buf;
|
|
}
|
|
|
|
public static SlotChange read(Buf in) {
|
|
return new SlotChange(in.readVarInt(), in.readVarInt(), in.readVarInt(),
|
|
in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The player's game mode, changed while the player is in the world.
|
|
*
|
|
* The mode arrives with the world join and can be changed afterwards, the
|
|
* way a server changes it. What a player may do about blocks follows from
|
|
* it: in survival the player breaks nothing, in creative the player
|
|
* breaks what they look at.
|
|
*/
|
|
public static final int SET_GAME_MODE = 0x1B;
|
|
|
|
/** Survival: the player cannot break blocks at all. */
|
|
public static final int GAMEMODE_SURVIVAL = 0;
|
|
|
|
/** Creative: the player breaks the block they are looking at. */
|
|
public static final int GAMEMODE_CREATIVE = 1;
|
|
|
|
public static final int DISCONNECT = 0x7F;
|
|
|
|
/** The client introducing itself, and naming the account it wants. */
|
|
public static final class Hello implements Packet {
|
|
public final int protocolVersion;
|
|
public final String account;
|
|
public final String clientName;
|
|
public final int clientVersion;
|
|
|
|
public Hello(int protocolVersion, String account, String clientName, int clientVersion) {
|
|
this.protocolVersion = protocolVersion;
|
|
this.account = account;
|
|
this.clientName = clientName;
|
|
this.clientVersion = clientVersion;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf();
|
|
buf.writeVarInt(this.protocolVersion);
|
|
buf.writeString(this.account);
|
|
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.readString(MAX_TEXT), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** The server accepting, and describing itself. */
|
|
public static final class HelloAck implements Packet {
|
|
public final int protocolVersion;
|
|
public final String serverName;
|
|
public final String motd;
|
|
public final int onlinePlayers;
|
|
public final int maxPlayers;
|
|
|
|
/**
|
|
* A value fresh for this connection.
|
|
*
|
|
* The proof is computed over it, so a recording of one login cannot be
|
|
* replayed against the next one.
|
|
*/
|
|
public final byte[] nonce;
|
|
|
|
/**
|
|
* The salt of the account that was named, when it exists.
|
|
*
|
|
* Sent before the client has proved anything, which does reveal whether
|
|
* an account exists. That is a deliberate trade for a design where the
|
|
* password never crosses the wire and the server stores no reversible
|
|
* form of it.
|
|
*/
|
|
public final byte[] salt;
|
|
|
|
public HelloAck(int protocolVersion, String serverName, String motd,
|
|
int onlinePlayers, int maxPlayers, byte[] nonce, byte[] salt) {
|
|
this.protocolVersion = protocolVersion;
|
|
this.serverName = serverName;
|
|
this.motd = motd;
|
|
this.onlinePlayers = onlinePlayers;
|
|
this.maxPlayers = maxPlayers;
|
|
this.nonce = nonce;
|
|
this.salt = salt;
|
|
}
|
|
|
|
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);
|
|
buf.writeBytes(this.nonce);
|
|
buf.writeBytes(this.salt);
|
|
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(),
|
|
in.readBytes(CHALLENGE_BYTES), in.readBytes(SALT_BYTES));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client claiming an account, with proof rather than with the password.
|
|
*
|
|
* The proof is HMAC-SHA256 over the derived key, the nonce and the account
|
|
* name. The password itself is never sent, the server stores no reversible
|
|
* form of it, and a proof captured from one connection is useless against
|
|
* the next because the nonce differs.
|
|
*/
|
|
public static final class Auth implements Packet {
|
|
public final String account;
|
|
public final byte[] proof;
|
|
|
|
public Auth(String account, byte[] proof) {
|
|
this.account = account;
|
|
this.proof = proof;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf();
|
|
buf.writeString(this.account);
|
|
buf.writeBytes(this.proof);
|
|
return buf;
|
|
}
|
|
|
|
public static Auth read(Buf in) {
|
|
return new Auth(in.readString(MAX_TEXT), in.readBytes(KEY_BYTES));
|
|
}
|
|
|
|
/** Derives the key from a password and salt the way the server does. */
|
|
public static byte[] deriveKey(char[] password, byte[] salt) {
|
|
try {
|
|
javax.crypto.SecretKeyFactory factory =
|
|
javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
|
javax.crypto.spec.PBEKeySpec spec = new javax.crypto.spec.PBEKeySpec(
|
|
password, salt, KEY_ITERATIONS, KEY_BYTES * 8);
|
|
try {
|
|
return factory.generateSecret(spec).getEncoded();
|
|
} finally {
|
|
// PBEKeySpec keeps its own copy of the password, and only
|
|
// clearPassword reaches it
|
|
spec.clearPassword();
|
|
}
|
|
} catch (java.security.NoSuchAlgorithmException
|
|
| java.security.spec.InvalidKeySpecException e) {
|
|
throw new IllegalStateException("PBKDF2WithHmacSHA256 is unavailable", e);
|
|
}
|
|
}
|
|
|
|
/** Builds the proof for a password, a salt and a nonce. */
|
|
public static byte[] proofFor(String account, char[] password, byte[] salt, byte[] nonce) {
|
|
byte[] key = deriveKey(password, salt);
|
|
try {
|
|
javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256");
|
|
mac.init(new javax.crypto.spec.SecretKeySpec(key, "HmacSHA256"));
|
|
mac.update(nonce);
|
|
mac.update(account.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
|
return mac.doFinal();
|
|
} catch (java.security.NoSuchAlgorithmException | java.security.InvalidKeyException e) {
|
|
throw new IllegalStateException("HmacSHA256 is unavailable", e);
|
|
} finally {
|
|
java.util.Arrays.fill(key, (byte) 0);
|
|
}
|
|
}
|
|
|
|
/** Compares two proofs without leaking how much matched. */
|
|
public static boolean proofsEqual(byte[] a, byte[] b) {
|
|
if (a == null || b == null || a.length != b.length) {
|
|
return false;
|
|
}
|
|
int diff = 0;
|
|
for (int i = 0; i < a.length; i++) {
|
|
diff |= a[i] ^ b[i];
|
|
}
|
|
return diff == 0;
|
|
}
|
|
}
|
|
|
|
/** Whether the account was accepted, and on what terms. */
|
|
public static final class AuthAck implements Packet {
|
|
public final boolean accepted;
|
|
public final String reason;
|
|
public final String token;
|
|
public final long expiresAt;
|
|
|
|
/**
|
|
* The token that makes the session longer when it runs out.
|
|
*
|
|
* It lives inside the game and for that: it does not touch the
|
|
* account's own reload token, so nobody has to sign in to the
|
|
* launcher again after every restart.
|
|
*/
|
|
public final String reloadToken;
|
|
|
|
/** When the reload token itself runs out. */
|
|
public final long reloadExpiresAt;
|
|
|
|
private AuthAck(boolean accepted, String reason, String token, long expiresAt,
|
|
String reloadToken, long reloadExpiresAt) {
|
|
this.accepted = accepted;
|
|
this.reason = reason;
|
|
this.token = token;
|
|
this.expiresAt = expiresAt;
|
|
this.reloadToken = reloadToken;
|
|
this.reloadExpiresAt = reloadExpiresAt;
|
|
}
|
|
|
|
public static AuthAck ok(String token, long expiresAt) {
|
|
return new AuthAck(true, "", token, expiresAt, "", 0L);
|
|
}
|
|
|
|
public static AuthAck ok(String token, long expiresAt, String reloadToken,
|
|
long reloadExpiresAt) {
|
|
return new AuthAck(true, "", token, expiresAt, reloadToken, reloadExpiresAt);
|
|
}
|
|
|
|
public static AuthAck refused(String reason) {
|
|
return new AuthAck(false, reason, "", 0L, "", 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);
|
|
buf.writeString(this.reloadToken);
|
|
buf.writeLong(this.reloadExpiresAt);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static AuthAck read(Buf in) {
|
|
boolean accepted = in.readBoolean();
|
|
// the reason is written either way, so it has to be read either way:
|
|
// skipping it here shifts everything after it by one field
|
|
String reason = in.readString(MAX_TEXT);
|
|
if (!accepted) {
|
|
return refused(reason);
|
|
}
|
|
return ok(in.readString(MAX_TEXT), in.readLong(), in.readString(MAX_TEXT),
|
|
in.readLong());
|
|
}
|
|
}
|
|
|
|
/** The client redeeming the token it was just handed. */
|
|
public static final class Login implements Packet {
|
|
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 implements Packet {
|
|
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) {
|
|
boolean accepted = in.readBoolean();
|
|
String reason = in.readString(MAX_TEXT);
|
|
if (!accepted) {
|
|
return refused(reason);
|
|
}
|
|
return ok(in.readVarInt(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** A reason, said out loud, before the connection goes away. */
|
|
public static final class Disconnect implements Packet {
|
|
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));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asking to be put in the world.
|
|
*
|
|
* The position it carries is where the client thinks it is, which is how a
|
|
* reconnect puts a player back where it was standing rather than at the
|
|
* spawn. A client that has never been here sends no position at all, and
|
|
* the server decides where it starts.
|
|
*/
|
|
public static final class JoinWorld implements Packet {
|
|
public final double x;
|
|
public final double y;
|
|
public final double z;
|
|
public final float yaw;
|
|
public final float pitch;
|
|
public final boolean hasPosition;
|
|
|
|
public JoinWorld(double x, double y, double z, float yaw, float pitch,
|
|
boolean hasPosition) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.yaw = yaw;
|
|
this.pitch = pitch;
|
|
this.hasPosition = hasPosition;
|
|
}
|
|
|
|
/** A client that has no place to come back to. */
|
|
public static JoinWorld fresh() {
|
|
return new JoinWorld(0.0D, 0.0D, 0.0D, 0.0F, 0.0F, false);
|
|
}
|
|
|
|
/** A client that knows where it is. */
|
|
public static JoinWorld at(double x, double y, double z, float yaw, float pitch) {
|
|
return new JoinWorld(x, y, z, yaw, pitch, true);
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf();
|
|
buf.writeBoolean(this.hasPosition);
|
|
if (this.hasPosition) {
|
|
buf.writePosition(this.x, this.y, this.z);
|
|
buf.writeFloat(this.yaw);
|
|
buf.writeFloat(this.pitch);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static JoinWorld read(Buf in) {
|
|
if (!in.readBoolean()) {
|
|
return fresh();
|
|
}
|
|
double[] at = in.readPosition();
|
|
return new JoinWorld(at[0], at[1], at[2], in.readFloat(), in.readFloat(), true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The world the client is in.
|
|
*
|
|
* The seed goes across so that the client can build the same sky, the same
|
|
* colours and the same cloud shapes the server sees, without the server
|
|
* having to send any of it.
|
|
*/
|
|
public static final class WorldJoin implements Packet {
|
|
public final int entityId;
|
|
public final long seed;
|
|
public final double spawnX;
|
|
public final double spawnY;
|
|
public final double spawnZ;
|
|
public final int viewDistance;
|
|
public final int worldHeight;
|
|
|
|
/** The game mode the player starts in. */
|
|
public final int gameMode;
|
|
|
|
public WorldJoin(int entityId, long seed, double spawnX, double spawnY,
|
|
double spawnZ, int viewDistance, int worldHeight, int gameMode) {
|
|
this.entityId = entityId;
|
|
this.seed = seed;
|
|
this.spawnX = spawnX;
|
|
this.spawnY = spawnY;
|
|
this.spawnZ = spawnZ;
|
|
this.viewDistance = viewDistance;
|
|
this.worldHeight = worldHeight;
|
|
this.gameMode = gameMode;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf();
|
|
buf.writeVarInt(this.entityId);
|
|
buf.writeVarLong(this.seed);
|
|
buf.writePosition(this.spawnX, this.spawnY, this.spawnZ);
|
|
buf.writeVarInt(this.viewDistance);
|
|
buf.writeVarInt(this.worldHeight);
|
|
buf.writeVarInt(this.gameMode);
|
|
return buf;
|
|
}
|
|
|
|
public static WorldJoin read(Buf in) {
|
|
int entityId = in.readVarInt();
|
|
long seed = in.readVarLong();
|
|
double[] spawn = in.readPosition();
|
|
return new WorldJoin(entityId, seed, spawn[0], spawn[1], spawn[2],
|
|
in.readVarInt(), in.readVarInt(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One chunk, on its way to the client.
|
|
*
|
|
* The block data is deflated, because a chunk is sixty five kilobytes of
|
|
* which ninety five percent is air, and sending that raw would make joining
|
|
* a server a matter of patience.
|
|
*/
|
|
public static final class ChunkData implements Packet {
|
|
public final int chunkX;
|
|
public final int chunkZ;
|
|
public final byte[] blocks;
|
|
|
|
public ChunkData(int chunkX, int chunkZ, byte[] blocks) {
|
|
if (blocks == null || blocks.length != ChunkFormat.VOLUME) {
|
|
throw new IllegalArgumentException("chunk data is "
|
|
+ (blocks == null ? "missing" : blocks.length + " bytes")
|
|
+ ", expected " + ChunkFormat.VOLUME);
|
|
}
|
|
this.chunkX = chunkX;
|
|
this.chunkZ = chunkZ;
|
|
this.blocks = blocks;
|
|
}
|
|
|
|
public Buf write() {
|
|
byte[] packed = ChunkFormat.compress(this.blocks);
|
|
Buf buf = new Buf(ChunkFormat.MAX_COMPRESSED);
|
|
buf.writeVarInt(this.chunkX);
|
|
buf.writeVarInt(this.chunkZ);
|
|
buf.writeVarInt(packed.length);
|
|
buf.writeBytes(packed);
|
|
return buf;
|
|
}
|
|
|
|
public static ChunkData read(Buf in) {
|
|
int chunkX = in.readVarInt();
|
|
int chunkZ = in.readVarInt();
|
|
int length = in.readVarInt();
|
|
if (length < 0 || length > ChunkFormat.MAX_COMPRESSED) {
|
|
throw new ProtocolException("compressed chunk of " + length
|
|
+ " bytes is out of range");
|
|
}
|
|
byte[] packed = in.readBytes(length);
|
|
return new ChunkData(chunkX, chunkZ,
|
|
ChunkFormat.decompress(packed, ChunkFormat.VOLUME));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One block, changed.
|
|
*
|
|
* The same packet in both directions. The client asks, the server says, and
|
|
* the body is the same either way, which is why there is no second class
|
|
* for the request.
|
|
*/
|
|
public static final class BlockChange implements Packet {
|
|
public final int x;
|
|
public final int y;
|
|
public final int z;
|
|
public final byte blockId;
|
|
|
|
public BlockChange(int x, int y, int z, byte blockId) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.blockId = blockId;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(16);
|
|
buf.writeVarInt(this.x);
|
|
buf.writeVarInt(this.y);
|
|
buf.writeVarInt(this.z);
|
|
buf.writeByte(this.blockId);
|
|
return buf;
|
|
}
|
|
|
|
public static BlockChange read(Buf in) {
|
|
return new BlockChange(in.readVarInt(), in.readVarInt(),
|
|
in.readVarInt(), (byte) in.readUnsignedByte());
|
|
}
|
|
}
|
|
|
|
/** Where the player is now. */
|
|
public static final class PlayerMove implements Packet {
|
|
public final double x;
|
|
public final double y;
|
|
public final double z;
|
|
public final float yaw;
|
|
public final float pitch;
|
|
public final boolean onGround;
|
|
|
|
public PlayerMove(double x, double y, double z, float yaw, float pitch,
|
|
boolean onGround) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.yaw = yaw;
|
|
this.pitch = pitch;
|
|
this.onGround = onGround;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(32);
|
|
buf.writePosition(this.x, this.y, this.z);
|
|
buf.writeFloat(this.yaw);
|
|
buf.writeFloat(this.pitch);
|
|
buf.writeBoolean(this.onGround);
|
|
return buf;
|
|
}
|
|
|
|
public static PlayerMove read(Buf in) {
|
|
double[] at = in.readPosition();
|
|
return new PlayerMove(at[0], at[1], at[2], in.readFloat(), in.readFloat(),
|
|
in.readBoolean());
|
|
}
|
|
}
|
|
|
|
/** Somebody else moved. */
|
|
public static final class EntityMove implements Packet {
|
|
public final int entityId;
|
|
public final double x;
|
|
public final double y;
|
|
public final double z;
|
|
public final float yaw;
|
|
public final float pitch;
|
|
|
|
public EntityMove(int entityId, double x, double y, double z,
|
|
float yaw, float pitch) {
|
|
this.entityId = entityId;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.yaw = yaw;
|
|
this.pitch = pitch;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(32);
|
|
buf.writeVarInt(this.entityId);
|
|
buf.writePosition(this.x, this.y, this.z);
|
|
buf.writeFloat(this.yaw);
|
|
buf.writeFloat(this.pitch);
|
|
return buf;
|
|
}
|
|
|
|
public static EntityMove read(Buf in) {
|
|
int entityId = in.readVarInt();
|
|
double[] at = in.readPosition();
|
|
return new EntityMove(entityId, at[0], at[1], at[2],
|
|
in.readFloat(), in.readFloat());
|
|
}
|
|
}
|
|
|
|
/** A liveness check. */
|
|
public static final class Ping implements Packet {
|
|
public final long id;
|
|
|
|
public Ping(long id) {
|
|
this.id = id;
|
|
}
|
|
|
|
public Buf write() {
|
|
return new Buf(8).writeLong(this.id);
|
|
}
|
|
|
|
public static Ping read(Buf in) {
|
|
return new Ping(in.readLong());
|
|
}
|
|
}
|
|
|
|
/** The answer to a liveness check. */
|
|
public static final class Pong implements Packet {
|
|
public final long id;
|
|
|
|
public Pong(long id) {
|
|
this.id = id;
|
|
}
|
|
|
|
public Buf write() {
|
|
return new Buf(8).writeLong(this.id);
|
|
}
|
|
|
|
public static Pong read(Buf in) {
|
|
return new Pong(in.readLong());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Somebody else is here.
|
|
*
|
|
* Sent when a player arrives, so that the others have something to draw.
|
|
* The player id is carried along with the entity id because the two are
|
|
* different things: the entity is what the client draws, the player is
|
|
* who the server knows the player to be.
|
|
*/
|
|
public static final class EntitySpawn implements Packet {
|
|
public final int entityId;
|
|
public final int playerId;
|
|
public final String name;
|
|
public final double x;
|
|
public final double y;
|
|
public final double z;
|
|
public final float yaw;
|
|
public final float pitch;
|
|
|
|
public EntitySpawn(int entityId, int playerId, String name,
|
|
double x, double y, double z, float yaw, float pitch) {
|
|
this.entityId = entityId;
|
|
this.playerId = playerId;
|
|
this.name = name;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.yaw = yaw;
|
|
this.pitch = pitch;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64);
|
|
buf.writeVarInt(this.entityId);
|
|
buf.writeVarInt(this.playerId);
|
|
buf.writeString(this.name);
|
|
buf.writePosition(this.x, this.y, this.z);
|
|
buf.writeFloat(this.yaw);
|
|
buf.writeFloat(this.pitch);
|
|
return buf;
|
|
}
|
|
|
|
public static EntitySpawn read(Buf in) {
|
|
int entityId = in.readVarInt();
|
|
int playerId = in.readVarInt();
|
|
String name = in.readString(MAX_TEXT);
|
|
double[] at = in.readPosition();
|
|
return new EntitySpawn(entityId, playerId, name, at[0], at[1], at[2],
|
|
in.readFloat(), in.readFloat());
|
|
}
|
|
}
|
|
|
|
/** Somebody else has left. */
|
|
public static final class EntityRemove implements Packet {
|
|
public final int entityId;
|
|
|
|
public EntityRemove(int entityId) {
|
|
this.entityId = entityId;
|
|
}
|
|
|
|
public Buf write() {
|
|
return new Buf(8).writeVarInt(this.entityId);
|
|
}
|
|
|
|
public static EntityRemove read(Buf in) {
|
|
return new EntityRemove(in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** The game mode of one player. */
|
|
public static final class GameMode implements Packet {
|
|
|
|
public final int mode;
|
|
|
|
public GameMode(int mode) {
|
|
this.mode = mode;
|
|
}
|
|
|
|
/** True when the player may break blocks. */
|
|
public boolean creative() {
|
|
return this.mode == GAMEMODE_CREATIVE;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8);
|
|
buf.writeVarInt(this.mode);
|
|
return buf;
|
|
}
|
|
|
|
public static GameMode read(Buf in) {
|
|
return new GameMode(in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One slot of an inventory.
|
|
*
|
|
* The empty slot is a stack of nothing, so there is no special case: an
|
|
* empty slot is one with the id of nothing and a count of zero.
|
|
*/
|
|
public static final class Slot {
|
|
|
|
public final int item;
|
|
public final int count;
|
|
public final int condition;
|
|
|
|
public Slot(int item, int count, int condition) {
|
|
this.item = item;
|
|
this.count = count;
|
|
this.condition = condition;
|
|
}
|
|
|
|
public static Slot of(ItemStack stack) {
|
|
if (stack == null || stack.isEmpty()) {
|
|
return new Slot(ItemRegistry.NOTHING, 0, -1);
|
|
}
|
|
return new Slot(stack.item, stack.count, stack.condition);
|
|
}
|
|
|
|
public void write(Buf buf) {
|
|
buf.writeVarInt(this.item);
|
|
buf.writeVarInt(this.count);
|
|
buf.writeVarInt(this.condition);
|
|
}
|
|
|
|
public static Slot read(Buf in) {
|
|
return new Slot(in.readVarInt(), in.readVarInt(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** The whole inventory of a player. */
|
|
public static final class Inventory implements Packet {
|
|
|
|
public final Slot[] slots;
|
|
|
|
/** The slot the player is holding. */
|
|
public final int held;
|
|
|
|
/**
|
|
* The clothing the player wears: seven places, two layers each.
|
|
*
|
|
* The order is the head, the face, the chest, the legs, the hands,
|
|
* the feet and the whole body, and inside each place the clothes
|
|
* under come before the armour over.
|
|
*/
|
|
public final Slot[] armor;
|
|
|
|
public Inventory(Slot[] slots, int held) {
|
|
this(slots, held, new Slot[0]);
|
|
}
|
|
|
|
public Inventory(Slot[] slots, int held, Slot[] armor) {
|
|
this.slots = slots;
|
|
this.held = held;
|
|
this.armor = armor == null ? new Slot[0] : armor;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(16 + (this.slots.length + this.armor.length) * 12);
|
|
buf.writeVarInt(this.held);
|
|
buf.writeVarInt(this.slots.length);
|
|
for (int i = 0; i < this.slots.length; i++) {
|
|
Slot slot = this.slots[i] == null ? Slot.of(null) : this.slots[i];
|
|
slot.write(buf);
|
|
}
|
|
buf.writeVarInt(this.armor.length);
|
|
for (int i = 0; i < this.armor.length; i++) {
|
|
Slot slot = this.armor[i] == null ? Slot.of(null) : this.armor[i];
|
|
slot.write(buf);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static Inventory read(Buf in) {
|
|
int held = in.readVarInt();
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 1024) {
|
|
throw new ProtocolException("an inventory of " + count + " slots is not real");
|
|
}
|
|
Slot[] slots = new Slot[count];
|
|
for (int i = 0; i < count; i++) {
|
|
slots[i] = Slot.read(in);
|
|
}
|
|
int worn = in.readVarInt();
|
|
if (worn < 0 || worn > 64) {
|
|
throw new ProtocolException("that much clothing is not real");
|
|
}
|
|
Slot[] armor = new Slot[worn];
|
|
for (int i = 0; i < worn; i++) {
|
|
armor[i] = Slot.read(in);
|
|
}
|
|
return new Inventory(slots, held, armor);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The launcher's session, as the game server sees it.
|
|
*
|
|
* The token is a bearer credential: whoever holds it is the player. It is
|
|
* sent to the game server and nowhere else, and the game server uses it
|
|
* once, to ask the launcher whether it is good.
|
|
*/
|
|
public static final class TokenAuth implements Packet {
|
|
public final String account;
|
|
public final String uuid;
|
|
public final String token;
|
|
|
|
public TokenAuth(String account, String uuid, String token) {
|
|
this.account = account;
|
|
this.uuid = uuid;
|
|
this.token = token;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(512);
|
|
buf.writeString(this.account);
|
|
buf.writeString(this.uuid);
|
|
buf.writeString(this.token);
|
|
return buf;
|
|
}
|
|
|
|
public static TokenAuth read(Buf in) {
|
|
return new TokenAuth(in.readString(MAX_TEXT), in.readString(MAX_TEXT),
|
|
in.readString(MAX_TEXT));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client starts or stops digging.
|
|
*
|
|
* The client does not decide anything: it says where it is looking and
|
|
* whether the button is down. The server works out what happens there,
|
|
* how long it takes and who is allowed to do it.
|
|
*/
|
|
public static final class Dig implements Packet {
|
|
|
|
/** The button went down. */
|
|
public static final int START = 0;
|
|
|
|
/** The button came up. */
|
|
public static final int STOP = 1;
|
|
|
|
public final int action;
|
|
public final int x;
|
|
public final int y;
|
|
public final int z;
|
|
public final int face;
|
|
|
|
public Dig(int action, int x, int y, int z, int face) {
|
|
this.action = action;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.face = face;
|
|
}
|
|
|
|
public boolean start() {
|
|
return this.action == START;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(24);
|
|
buf.writeVarInt(this.action);
|
|
buf.writeVarInt(this.x);
|
|
buf.writeVarInt(this.y);
|
|
buf.writeVarInt(this.z);
|
|
buf.writeVarInt(this.face);
|
|
return buf;
|
|
}
|
|
|
|
public static Dig read(Buf in) {
|
|
return new Dig(in.readVarInt(), in.readVarInt(), in.readVarInt(),
|
|
in.readVarInt(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* How far the digging has come, so that everybody sees the cracks.
|
|
*
|
|
* The stage goes from zero to nine. A stage of minus one means that
|
|
* nothing is being dug at that place.
|
|
*/
|
|
public static final class BreakProgress implements Packet {
|
|
|
|
/** The stage of the cracks, from zero to nine. */
|
|
public static final int STAGES = 10;
|
|
|
|
public final int x;
|
|
public final int y;
|
|
public final int z;
|
|
public final int stage;
|
|
|
|
public BreakProgress(int x, int y, int z, int stage) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.stage = stage;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(24);
|
|
buf.writeVarInt(this.x);
|
|
buf.writeVarInt(this.y);
|
|
buf.writeVarInt(this.z);
|
|
buf.writeVarInt(this.stage);
|
|
return buf;
|
|
}
|
|
|
|
public static BreakProgress read(Buf in) {
|
|
return new BreakProgress(in.readVarInt(), in.readVarInt(), in.readVarInt(),
|
|
in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asks to make something.
|
|
*
|
|
* The server looks the recipe up, checks the workbench and the knowledge
|
|
* of the player, and answers with a {@link CraftUpdate}.
|
|
*/
|
|
public static final class CraftStart implements Packet {
|
|
|
|
public final int item;
|
|
public final int count;
|
|
|
|
public CraftStart(int item, int count) {
|
|
this.item = item;
|
|
this.count = count;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8);
|
|
buf.writeVarInt(this.item);
|
|
buf.writeVarInt(this.count);
|
|
return buf;
|
|
}
|
|
|
|
public static CraftStart read(Buf in) {
|
|
return new CraftStart(in.readVarInt(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* What the server says about a craft.
|
|
*
|
|
* The seconds left go down while the work goes on. When it reaches zero
|
|
* the item is in the inventory. A reason that is not empty means that
|
|
* nothing was made, and says why.
|
|
*/
|
|
public static final class CraftUpdate implements Packet {
|
|
|
|
public final int item;
|
|
public final int count;
|
|
public final int secondsLeft;
|
|
public final String reason;
|
|
|
|
public CraftUpdate(int item, int count, int secondsLeft, String reason) {
|
|
this.item = item;
|
|
this.count = count;
|
|
this.secondsLeft = secondsLeft;
|
|
this.reason = reason == null ? "" : reason;
|
|
}
|
|
|
|
public boolean ok() {
|
|
return this.reason.isEmpty();
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64);
|
|
buf.writeVarInt(this.item);
|
|
buf.writeVarInt(this.count);
|
|
buf.writeVarInt(this.secondsLeft);
|
|
buf.writeString(this.reason);
|
|
return buf;
|
|
}
|
|
|
|
public static CraftUpdate read(Buf in) {
|
|
return new CraftUpdate(in.readVarInt(), in.readVarInt(), in.readVarInt(),
|
|
in.readString(MAX_TEXT));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asks to research an item at the research table.
|
|
*
|
|
* The server checks that the player has the item and the scrap, takes
|
|
* both and answers with a {@link ResearchUpdate}.
|
|
*/
|
|
public static final class ResearchStart implements Packet {
|
|
|
|
public final int item;
|
|
|
|
public ResearchStart(int item) {
|
|
this.item = item;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8);
|
|
buf.writeVarInt(this.item);
|
|
return buf;
|
|
}
|
|
|
|
public static ResearchStart read(Buf in) {
|
|
return new ResearchStart(in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** What the server says about a research. */
|
|
public static final class ResearchUpdate implements Packet {
|
|
|
|
public final int item;
|
|
public final int secondsLeft;
|
|
public final String reason;
|
|
|
|
public ResearchUpdate(int item, int secondsLeft, String reason) {
|
|
this.item = item;
|
|
this.secondsLeft = secondsLeft;
|
|
this.reason = reason == null ? "" : reason;
|
|
}
|
|
|
|
public boolean ok() {
|
|
return this.reason.isEmpty();
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64);
|
|
buf.writeVarInt(this.item);
|
|
buf.writeVarInt(this.secondsLeft);
|
|
buf.writeString(this.reason);
|
|
return buf;
|
|
}
|
|
|
|
public static ResearchUpdate read(Buf in) {
|
|
return new ResearchUpdate(in.readVarInt(), in.readVarInt(), in.readString(MAX_TEXT));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asks to open a node of the tree of a workbench.
|
|
*
|
|
* The server checks that the branch is open and that there is enough
|
|
* scrap, and takes the scrap.
|
|
*/
|
|
public static final class TechTreeUnlock implements Packet {
|
|
|
|
public final int item;
|
|
|
|
public TechTreeUnlock(int item) {
|
|
this.item = item;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8);
|
|
buf.writeVarInt(this.item);
|
|
return buf;
|
|
}
|
|
|
|
public static TechTreeUnlock read(Buf in) {
|
|
return new TechTreeUnlock(in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** The recipes the player knows. */
|
|
public static final class KnownRecipes implements Packet {
|
|
|
|
public final int[] items;
|
|
|
|
public KnownRecipes(int[] items) {
|
|
this.items = items;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8 + this.items.length * 4);
|
|
buf.writeVarInt(this.items.length);
|
|
for (int i = 0; i < this.items.length; i++) {
|
|
buf.writeVarInt(this.items[i]);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static KnownRecipes read(Buf in) {
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 4096) {
|
|
throw new ProtocolException("that many recipes is not real");
|
|
}
|
|
int[] items = new int[count];
|
|
for (int i = 0; i < count; i++) {
|
|
items[i] = in.readVarInt();
|
|
}
|
|
return new KnownRecipes(items);
|
|
}
|
|
}
|
|
|
|
/** The nodes of the trees the player has opened. */
|
|
public static final class TechTreeState implements Packet {
|
|
|
|
public final int[] items;
|
|
|
|
public TechTreeState(int[] items) {
|
|
this.items = items;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8 + this.items.length * 4);
|
|
buf.writeVarInt(this.items.length);
|
|
for (int i = 0; i < this.items.length; i++) {
|
|
buf.writeVarInt(this.items[i]);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static TechTreeState read(Buf in) {
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 4096) {
|
|
throw new ProtocolException("that many nodes is not real");
|
|
}
|
|
int[] items = new int[count];
|
|
for (int i = 0; i < count; i++) {
|
|
items[i] = in.readVarInt();
|
|
}
|
|
return new TechTreeState(items);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A container the player may look into: a crate of food on a junk pile,
|
|
* a chest, a barrel.
|
|
*/
|
|
public static final class ContainerOpen implements Packet {
|
|
|
|
public final int id;
|
|
public final String title;
|
|
public final Slot[] slots;
|
|
|
|
public ContainerOpen(int id, String title, Slot[] slots) {
|
|
this.id = id;
|
|
this.title = title == null ? "" : title;
|
|
this.slots = slots;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64 + this.slots.length * 12);
|
|
buf.writeVarInt(this.id);
|
|
buf.writeString(this.title);
|
|
buf.writeVarInt(this.slots.length);
|
|
for (int i = 0; i < this.slots.length; i++) {
|
|
Slot slot = this.slots[i] == null ? Slot.of(null) : this.slots[i];
|
|
slot.write(buf);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static ContainerOpen read(Buf in) {
|
|
int id = in.readVarInt();
|
|
String title = in.readString(MAX_TEXT);
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 1024) {
|
|
throw new ProtocolException("a container of " + count + " slots is not real");
|
|
}
|
|
Slot[] slots = new Slot[count];
|
|
for (int i = 0; i < count; i++) {
|
|
slots[i] = Slot.read(in);
|
|
}
|
|
return new ContainerOpen(id, title, slots);
|
|
}
|
|
}
|
|
|
|
/** The client takes something out of a container. */
|
|
public static final class ContainerTake implements Packet {
|
|
|
|
public final int id;
|
|
public final int slot;
|
|
|
|
public ContainerTake(int id, int slot) {
|
|
this.id = id;
|
|
this.slot = slot;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8);
|
|
buf.writeVarInt(this.id);
|
|
buf.writeVarInt(this.slot);
|
|
return buf;
|
|
}
|
|
|
|
public static ContainerTake read(Buf in) {
|
|
return new ContainerTake(in.readVarInt(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asks to learn a blueprint.
|
|
*
|
|
* A blueprint is the item with a mark on it. Learning it uses it up and
|
|
* opens the recipe for good.
|
|
*/
|
|
public static final class LearnBlueprint implements Packet {
|
|
|
|
public final int item;
|
|
|
|
public LearnBlueprint(int item) {
|
|
this.item = item;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(8);
|
|
buf.writeVarInt(this.item);
|
|
return buf;
|
|
}
|
|
|
|
public static LearnBlueprint read(Buf in) {
|
|
return new LearnBlueprint(in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/** The boxes of one model. */
|
|
public static final class HitboxModel {
|
|
|
|
public final String name;
|
|
public final Hitbox[] boxes;
|
|
|
|
public HitboxModel(String name, Hitbox[] boxes) {
|
|
this.name = name;
|
|
this.boxes = boxes;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The boxes of every model the server knows.
|
|
*
|
|
* The file with the boxes is the editor: it is read by the server, sent
|
|
* to the client and drawn there, so that a change in the file can be
|
|
* seen without anything else being touched.
|
|
*/
|
|
public static final class Hitboxes implements Packet {
|
|
|
|
public final HitboxModel[] models;
|
|
|
|
public Hitboxes(HitboxModel[] models) {
|
|
this.models = models;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(256 + this.models.length * 64);
|
|
buf.writeVarInt(this.models.length);
|
|
for (int i = 0; i < this.models.length; i++) {
|
|
HitboxModel model = this.models[i];
|
|
buf.writeString(model.name);
|
|
buf.writeVarInt(model.boxes.length);
|
|
for (int j = 0; j < model.boxes.length; j++) {
|
|
Hitbox box = model.boxes[j];
|
|
buf.writeString(box.name);
|
|
buf.writeString(box.part);
|
|
buf.writeFloat(box.minX);
|
|
buf.writeFloat(box.minY);
|
|
buf.writeFloat(box.minZ);
|
|
buf.writeFloat(box.maxX);
|
|
buf.writeFloat(box.maxY);
|
|
buf.writeFloat(box.maxZ);
|
|
buf.writeFloat(box.yaw);
|
|
}
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static Hitboxes read(Buf in) {
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 4096) {
|
|
throw new ProtocolException("that many box models is not real");
|
|
}
|
|
HitboxModel[] models = new HitboxModel[count];
|
|
for (int i = 0; i < count; i++) {
|
|
String name = in.readString(MAX_TEXT);
|
|
int boxes = in.readVarInt();
|
|
if (boxes < 0 || boxes > 256) {
|
|
throw new ProtocolException("that many boxes is not real");
|
|
}
|
|
Hitbox[] list = new Hitbox[boxes];
|
|
for (int j = 0; j < boxes; j++) {
|
|
String boxName = in.readString(MAX_TEXT);
|
|
String part = in.readString(MAX_TEXT);
|
|
list[j] = new Hitbox(boxName, part,
|
|
in.readFloat(), in.readFloat(), in.readFloat(),
|
|
in.readFloat(), in.readFloat(), in.readFloat(),
|
|
in.readFloat());
|
|
}
|
|
models[i] = new HitboxModel(name, list);
|
|
}
|
|
return new Hitboxes(models);
|
|
}
|
|
}
|
|
|
|
/** One model standing somewhere in the world. */
|
|
public static final class HitboxInstance {
|
|
|
|
public final String model;
|
|
public final float x;
|
|
public final float y;
|
|
public final float z;
|
|
public final float yaw;
|
|
|
|
public HitboxInstance(String model, float x, float y, float z, float yaw) {
|
|
this.model = model;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.yaw = yaw;
|
|
}
|
|
}
|
|
|
|
/** Every place where a model with boxes stands. */
|
|
public static final class HitboxInstances implements Packet {
|
|
|
|
public final HitboxInstance[] instances;
|
|
|
|
public HitboxInstances(HitboxInstance[] instances) {
|
|
this.instances = instances;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(32 + this.instances.length * 32);
|
|
buf.writeVarInt(this.instances.length);
|
|
for (int i = 0; i < this.instances.length; i++) {
|
|
HitboxInstance instance = this.instances[i];
|
|
buf.writeString(instance.model);
|
|
buf.writeFloat(instance.x);
|
|
buf.writeFloat(instance.y);
|
|
buf.writeFloat(instance.z);
|
|
buf.writeFloat(instance.yaw);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static HitboxInstances read(Buf in) {
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 65536) {
|
|
throw new ProtocolException("that many box instances is not real");
|
|
}
|
|
HitboxInstance[] instances = new HitboxInstance[count];
|
|
for (int i = 0; i < count; i++) {
|
|
String model = in.readString(MAX_TEXT);
|
|
instances[i] = new HitboxInstance(model, in.readFloat(), in.readFloat(),
|
|
in.readFloat(), in.readFloat());
|
|
}
|
|
return new HitboxInstances(instances);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asks the server to read its box file again.
|
|
*
|
|
* The file is the editor, so after a change the client wants the new
|
|
* numbers without the server having to be restarted.
|
|
*/
|
|
public static final class HitboxesReload implements Packet {
|
|
|
|
public Buf write() {
|
|
return new Buf(4);
|
|
}
|
|
|
|
public static HitboxesReload read(Buf in) {
|
|
return new HitboxesReload();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client hits something.
|
|
*
|
|
* The place of the hit matters: a hit on the head hurts twice as much as
|
|
* a hit on the body, and the clothing only takes a part of the harm away
|
|
* where it covers the body.
|
|
*/
|
|
public static final class Attack implements Packet {
|
|
|
|
/** The hand that holds the weapon. */
|
|
public static final int HAND_MAIN = 0;
|
|
public static final int HAND_OFF = 1;
|
|
|
|
public final int target;
|
|
public final float x;
|
|
public final float y;
|
|
public final float z;
|
|
public final int hand;
|
|
|
|
public Attack(int target, float x, float y, float z, int hand) {
|
|
this.target = target;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.hand = hand;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(24);
|
|
buf.writeVarInt(this.target);
|
|
buf.writeFloat(this.x);
|
|
buf.writeFloat(this.y);
|
|
buf.writeFloat(this.z);
|
|
buf.writeVarInt(this.hand);
|
|
return buf;
|
|
}
|
|
|
|
public static Attack read(Buf in) {
|
|
return new Attack(in.readVarInt(), in.readFloat(), in.readFloat(),
|
|
in.readFloat(), in.readVarInt());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* What somebody said.
|
|
*
|
|
* The name of the sender is here, because the server is the one who
|
|
* decides who said what: a client cannot claim to be somebody else.
|
|
*/
|
|
public static final class Chat implements Packet {
|
|
|
|
/** The name of the sender, or empty when the server speaks. */
|
|
public final String from;
|
|
|
|
public final String text;
|
|
|
|
public Chat(String from, String text) {
|
|
this.from = from == null ? "" : from;
|
|
this.text = text == null ? "" : text;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64 + this.text.length());
|
|
buf.writeString(this.from);
|
|
buf.writeString(this.text);
|
|
return buf;
|
|
}
|
|
|
|
public static Chat read(Buf in) {
|
|
return new Chat(in.readString(MAX_TEXT), in.readString(MAX_TEXT));
|
|
}
|
|
}
|
|
|
|
/** The client says something. */
|
|
public static final class ChatSend implements Packet {
|
|
|
|
public final String text;
|
|
|
|
public ChatSend(String text) {
|
|
this.text = text == null ? "" : text;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64 + this.text.length());
|
|
buf.writeString(this.text);
|
|
return buf;
|
|
}
|
|
|
|
public static ChatSend read(Buf in) {
|
|
return new ChatSend(in.readString(MAX_TEXT));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Who is in the world.
|
|
*
|
|
* The whole list, every time it changes: the list of a server with a
|
|
* handful of players is small, and a full list cannot get out of step
|
|
* with itself the way a difference can.
|
|
*/
|
|
public static final class PlayerList implements Packet {
|
|
|
|
public final String[] names;
|
|
|
|
/** The game mode of each player, in the same order. */
|
|
public final int[] gameModes;
|
|
|
|
public PlayerList(String[] names, int[] gameModes) {
|
|
this.names = names;
|
|
this.gameModes = gameModes;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(32 + this.names.length * 24);
|
|
buf.writeVarInt(this.names.length);
|
|
for (int i = 0; i < this.names.length; i++) {
|
|
buf.writeString(this.names[i]);
|
|
buf.writeVarInt(i < this.gameModes.length ? this.gameModes[i] : 0);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static PlayerList read(Buf in) {
|
|
int count = in.readVarInt();
|
|
if (count < 0 || count > 4096) {
|
|
throw new ProtocolException("that many players is not real");
|
|
}
|
|
String[] names = new String[count];
|
|
int[] gameModes = new int[count];
|
|
for (int i = 0; i < count; i++) {
|
|
names[i] = in.readString(MAX_TEXT);
|
|
gameModes[i] = in.readVarInt();
|
|
}
|
|
return new PlayerList(names, gameModes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The client asks for a longer session.
|
|
*
|
|
* The token in it is the one the server handed out with the session. It
|
|
* lives inside the game and for that: it does not touch the account's own
|
|
* reload token.
|
|
*/
|
|
public static final class SessionReload implements Packet {
|
|
|
|
public final String token;
|
|
|
|
public SessionReload(String token) {
|
|
this.token = token == null ? "" : token;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(64 + this.token.length());
|
|
buf.writeString(this.token);
|
|
return buf;
|
|
}
|
|
|
|
public static SessionReload read(Buf in) {
|
|
return new SessionReload(in.readString(MAX_TEXT));
|
|
}
|
|
}
|
|
|
|
/** The session was made longer. */
|
|
public static final class SessionRenewed implements Packet {
|
|
|
|
public final boolean accepted;
|
|
public final String reason;
|
|
public final String token;
|
|
public final long expiresAt;
|
|
public final String reloadToken;
|
|
public final long reloadExpiresAt;
|
|
|
|
public SessionRenewed(boolean accepted, String reason, String token, long expiresAt,
|
|
String reloadToken, long reloadExpiresAt) {
|
|
this.accepted = accepted;
|
|
this.reason = reason == null ? "" : reason;
|
|
this.token = token == null ? "" : token;
|
|
this.expiresAt = expiresAt;
|
|
this.reloadToken = reloadToken == null ? "" : reloadToken;
|
|
this.reloadExpiresAt = reloadExpiresAt;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(128);
|
|
buf.writeBoolean(this.accepted);
|
|
buf.writeString(this.reason);
|
|
if (this.accepted) {
|
|
buf.writeString(this.token);
|
|
buf.writeLong(this.expiresAt);
|
|
buf.writeString(this.reloadToken);
|
|
buf.writeLong(this.reloadExpiresAt);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
public static SessionRenewed read(Buf in) {
|
|
boolean accepted = in.readBoolean();
|
|
String reason = in.readString(MAX_TEXT);
|
|
if (!accepted) {
|
|
return new SessionRenewed(false, reason, "", 0L, "", 0L);
|
|
}
|
|
return new SessionRenewed(true, reason, in.readString(MAX_TEXT), in.readLong(),
|
|
in.readString(MAX_TEXT), in.readLong());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the player is one of the administrators.
|
|
*
|
|
* The server is the one who decides, and it can take the rights away
|
|
* while the player is looking at the world.
|
|
*/
|
|
public static final class AdminState implements Packet {
|
|
|
|
public final boolean admin;
|
|
|
|
public AdminState(boolean admin) {
|
|
this.admin = admin;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(4);
|
|
buf.writeBoolean(this.admin);
|
|
return buf;
|
|
}
|
|
|
|
public static AdminState read(Buf in) {
|
|
return new AdminState(in.readBoolean());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A piece of the far view.
|
|
*
|
|
* The far view is the ground of the world seen from far away: a grid of
|
|
* heights and colours, one sample every few blocks. The client turns it
|
|
* into a mesh and draws it beyond the chunks that are loaded.
|
|
*/
|
|
public static final class LodRegion implements Packet {
|
|
|
|
/** The block the region starts at. */
|
|
public final int originX;
|
|
public final int originZ;
|
|
|
|
/** How many blocks there are between two samples. */
|
|
public final int step;
|
|
|
|
/** How many samples there are on a side. */
|
|
public final int size;
|
|
|
|
/** The height of every sample, in blocks. */
|
|
public final byte[] heights;
|
|
|
|
/** The colour of every sample: three bytes each, red green blue. */
|
|
public final byte[] colors;
|
|
|
|
public LodRegion(int originX, int originZ, int step, int size, byte[] heights,
|
|
byte[] colors) {
|
|
this.originX = originX;
|
|
this.originZ = originZ;
|
|
this.step = step;
|
|
this.size = size;
|
|
this.heights = heights;
|
|
this.colors = colors;
|
|
}
|
|
|
|
public Buf write() {
|
|
Buf buf = new Buf(32 + this.heights.length + this.colors.length);
|
|
buf.writeVarInt(this.originX);
|
|
buf.writeVarInt(this.originZ);
|
|
buf.writeVarInt(this.step);
|
|
buf.writeVarInt(this.size);
|
|
buf.writeBytes(this.heights);
|
|
buf.writeBytes(this.colors);
|
|
return buf;
|
|
}
|
|
|
|
public static LodRegion read(Buf in) {
|
|
int originX = in.readVarInt();
|
|
int originZ = in.readVarInt();
|
|
int step = in.readVarInt();
|
|
int size = in.readVarInt();
|
|
if (size < 0 || size > 1024) {
|
|
throw new ProtocolException("that many samples is not real");
|
|
}
|
|
byte[] heights = in.readBytes(size * size);
|
|
byte[] colors = in.readBytes(size * size * 3);
|
|
return new LodRegion(originX, originZ, step, size, heights, colors);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(JOIN_WORLD), "JoinWorld");
|
|
map.put(Integer.valueOf(WORLD_JOIN), "WorldJoin");
|
|
map.put(Integer.valueOf(CHUNK_DATA), "ChunkData");
|
|
map.put(Integer.valueOf(BLOCK_CHANGE), "BlockChange");
|
|
map.put(Integer.valueOf(PLAYER_MOVE), "PlayerMove");
|
|
map.put(Integer.valueOf(ENTITY_MOVE), "EntityMove");
|
|
map.put(Integer.valueOf(PING), "Ping");
|
|
map.put(Integer.valueOf(PONG), "Pong");
|
|
map.put(Integer.valueOf(ENTITY_SPAWN), "EntitySpawn");
|
|
map.put(Integer.valueOf(ENTITY_REMOVE), "EntityRemove");
|
|
map.put(Integer.valueOf(TOKEN_AUTH), "TokenAuth");
|
|
map.put(Integer.valueOf(SET_GAME_MODE), "GameMode");
|
|
map.put(Integer.valueOf(INVENTORY), "Inventory");
|
|
map.put(Integer.valueOf(INVENTORY_SLOT), "SlotChange");
|
|
map.put(Integer.valueOf(DIG), "Dig");
|
|
map.put(Integer.valueOf(BREAK_PROGRESS), "BreakProgress");
|
|
map.put(Integer.valueOf(CRAFT_START), "CraftStart");
|
|
map.put(Integer.valueOf(CRAFT_UPDATE), "CraftUpdate");
|
|
map.put(Integer.valueOf(RESEARCH_START), "ResearchStart");
|
|
map.put(Integer.valueOf(RESEARCH_UPDATE), "ResearchUpdate");
|
|
map.put(Integer.valueOf(TECH_TREE_UNLOCK), "TechTreeUnlock");
|
|
map.put(Integer.valueOf(KNOWN_RECIPES), "KnownRecipes");
|
|
map.put(Integer.valueOf(TECH_TREE_STATE), "TechTreeState");
|
|
map.put(Integer.valueOf(CONTAINER_OPEN), "ContainerOpen");
|
|
map.put(Integer.valueOf(CONTAINER_TAKE), "ContainerTake");
|
|
map.put(Integer.valueOf(LEARN_BLUEPRINT), "LearnBlueprint");
|
|
map.put(Integer.valueOf(HITBOXES), "Hitboxes");
|
|
map.put(Integer.valueOf(HITBOX_INSTANCES), "HitboxInstances");
|
|
map.put(Integer.valueOf(HITBOXES_RELOAD), "HitboxesReload");
|
|
map.put(Integer.valueOf(LOD_REGION), "LodRegion");
|
|
map.put(Integer.valueOf(ATTACK), "Attack");
|
|
map.put(Integer.valueOf(CHAT), "Chat");
|
|
map.put(Integer.valueOf(CHAT_SEND), "ChatSend");
|
|
map.put(Integer.valueOf(PLAYER_LIST), "PlayerList");
|
|
map.put(Integer.valueOf(SESSION_RELOAD), "SessionReload");
|
|
map.put(Integer.valueOf(SESSION_RENEWED), "SessionRenewed");
|
|
map.put(Integer.valueOf(ADMIN_STATE), "AdminState");
|
|
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
|
|
return map;
|
|
}
|
|
} |