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: * *
 *   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
 * 
* * After that the play phase begins, and it has no fixed order: * *
 *   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
 * 
*/ 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 LAUNCHER_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; /** The client hits something. */ public static final int ATTACK = 0x2D; /** * 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; 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) { 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()); } } /** 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 LauncherAuth implements Packet { public final String account; public final String uuid; public final String token; public LauncherAuth(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 LauncherAuth read(Buf in) { return new LauncherAuth(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()); } } /** * 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 names() { Map map = new LinkedHashMap(); 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(LAUNCHER_AUTH), "LauncherAuth"); 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(ATTACK), "Attack"); map.put(Integer.valueOf(DISCONNECT), "Disconnect"); return map; } }