package net.rustnotmc.protocol; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * Tests for the wire format. * * These are not example code. Every case here is something a hostile or simply * broken peer can actually do, and the point of the test is that the server * survives it without an OutOfMemoryError, an endless loop or a silent * misinterpretation. */ public final class ProtocolTest { private static int checks; private static int failed; private static final List failures = new ArrayList(); public static void main(String[] args) { run("varint round trip", new Runnable() { public void run() { testVarInt(); } }); run("varint rejects overlong input", new Runnable() { public void run() { testVarIntOverlong(); } }); run("negative numbers stay small", new Runnable() { public void run() { testVarIntZigZag(); } }); run("varlong round trip", new Runnable() { public void run() { testVarLong(); } }); run("fixed width round trip", new Runnable() { public void run() { testFixedWidth(); } }); run("string round trip incl. non ascii", new Runnable() { public void run() { testString(); } }); run("string length is bounded", new Runnable() { public void run() { testStringBound(); } }); run("reading past the end fails", new Runnable() { public void run() { testUnderflow(); } }); run("position survives the round trip", new Runnable() { public void run() { testPosition(); } }); run("frame round trip", new Runnable() { public void run() { testFrame(); } }); run("partial frame decodes to nothing", new Runnable() { public void run() { testPartialFrame(); } }); run("foreign magic is refused", new Runnable() { public void run() { testWrongMagic(); } }); run("oversized declared body is refused", new Runnable() { public void run() { testOversizedClaim(); } }); run("byte at a time still works", new Runnable() { public void run() { testSplitEverywhere(); } }); run("many frames in one read", new Runnable() { public void run() { testBatch(); } }); run("handshake packets round trip", new Runnable() { public void run() { testHandshake(); } }); run("play packets round trip", new Runnable() { public void run() { testPlayPackets(); } }); run("a chunk survives compression", new Runnable() { public void run() { testChunkCompression(); } }); run("a chunk that is not compressed data is refused", new Runnable() { public void run() { testBadChunkData(); } }); run("a truncated chunk is refused", new Runnable() { public void run() { testShortChunk(); } }); run("every packet has a name", new Runnable() { public void run() { testPacketNames(); } }); run("the items match the original game", new Runnable() { public void run() { testItems(); } }); run("an inventory survives the wire", new Runnable() { public void run() { testInventory(); } }); run("a lying length cannot stall the heap", new Runnable() { public void run() { testLyingLength(); } }); System.out.println(); for (int i = 0; i < failures.size(); i++) { System.out.println(" FAIL " + failures.get(i)); } System.out.println(); System.out.println("checks: " + checks + ", failed: " + failed); if (failed != 0) { System.out.println("FAILED"); System.exit(1); } System.out.println("OK"); } private static void run(String name, Runnable body) { checks++; int before = failed; try { body.run(); } catch (Throwable t) { failed++; failures.add(name + ": threw " + t); t.printStackTrace(); return; } if (failed == before) { System.out.println(" ok " + name); } else { failures.add(name + ": assertions failed"); } } private static void eq(Object what, Object expected, Object actual) { // arrays compare by reference under equals(), which is never what a // caller means when it hands over a body if (expected instanceof byte[] && actual instanceof byte[]) { if (!java.util.Arrays.equals((byte[]) expected, (byte[]) actual)) { failed++; failures.add(what + ": byte arrays differ"); } return; } if (expected == null ? actual != null : !expected.equals(actual)) { failed++; failures.add(what + ": expected " + expected + " but got " + actual); } } private static void yes(String what, boolean condition) { if (!condition) { failed++; failures.add(what); } } private static void refuses(String what, Runnable body) { try { body.run(); failed++; failures.add(what + ": accepted something it should have refused"); } catch (ProtocolException expected) { // exactly what we want } catch (Throwable other) { failed++; failures.add(what + ": threw " + other + " instead of ProtocolException"); } } // ------------------------------------------------------------- the tests private static void testVarInt() { int[] values = {0, 1, -1, 127, 128, -128, 12345, -12345, Integer.MAX_VALUE, Integer.MIN_VALUE}; for (int i = 0; i < values.length; i++) { Buf buf = new Buf(); buf.writeVarInt(values[i]); eq("varint " + values[i], Integer.valueOf(values[i]), Integer.valueOf(new Buf(buf.toByteArray()).readVarInt())); } } private static void testVarIntOverlong() { // ten bytes of 0xFF: a five byte value cannot be this long byte[] evil = new byte[10]; java.util.Arrays.fill(evil, (byte) 0xFF); refuses("overlong varint", new Runnable() { public void run() { new Buf(evil).readVarInt(); } }); } private static void testVarIntZigZag() { Buf small = new Buf(); small.writeVarInt(-1); yes("minus one is one byte, not five", small.writerIndex() == 1); Buf zero = new Buf(); zero.writeVarInt(0); yes("zero is one byte", zero.writerIndex() == 1); Buf positive = new Buf(); positive.writeVarInt(63); Buf negative = new Buf(); negative.writeVarInt(-63); eq("63 and -63 cost the same", Integer.valueOf(positive.writerIndex()), Integer.valueOf(negative.writerIndex())); } private static void testVarLong() { long[] values = {0L, 1L, -1L, Long.MAX_VALUE, Long.MIN_VALUE, -1234567890123L}; for (int i = 0; i < values.length; i++) { Buf buf = new Buf(); buf.writeVarLong(values[i]); eq("varlong " + values[i], Long.valueOf(values[i]), Long.valueOf(new Buf(buf.toByteArray()).readVarLong())); } } private static void testFixedWidth() { Buf buf = new Buf(); buf.writeBoolean(true); buf.writeByte(0x7F); buf.writeShort(0xBEEF); buf.writeInt(-559038737); buf.writeLong(-1234567890123L); buf.writeFloat(0.5F); buf.writeDouble(-2.25D); Buf read = new Buf(buf.toByteArray()); yes("boolean", read.readBoolean()); eq("byte", Integer.valueOf(0x7F), Integer.valueOf(read.readUnsignedByte())); eq("short", Integer.valueOf(0xBEEF), Integer.valueOf(read.readShort())); eq("int", Integer.valueOf(-559038737), Integer.valueOf(read.readInt())); eq("long", Long.valueOf(-1234567890123L), Long.valueOf(read.readLong())); eq("float", Float.valueOf(0.5F), Float.valueOf(read.readFloat())); eq("double", Double.valueOf(-2.25D), Double.valueOf(read.readDouble())); eq("nothing left", Integer.valueOf(0), Integer.valueOf(read.readableBytes())); } private static void testString() { String[] values = {"", "player", "Игрок", "a name with spaces", "~!@#$%^&*()"}; for (int i = 0; i < values.length; i++) { Buf buf = new Buf(); buf.writeString(values[i]); eq("string " + values[i], values[i], new Buf(buf.toByteArray()).readString(Packets.MAX_TEXT)); } } private static void testStringBound() { // announces a huge string and then does not send it Buf buf = new Buf(); buf.writeVarInt(0x7FFFFFF0); refuses("string longer than the limit", new Runnable() { public void run() { buf.readString(Packets.MAX_TEXT); } }); } private static void testUnderflow() { refuses("reading past the end", new Runnable() { public void run() { new Buf(new byte[2]).readInt(); } }); } private static void testPosition() { // Y is deliberately whole blocks: it only has 12 bits, so it is stored // as a signed block number and a fractional Y is quantised away double[][] positions = { {0.0D, 0.0D, 0.0D}, {1.5D, -64.0D, 300.25D}, {-1234.5D, 319.0D, -4096.75D}, {0.03125D, -2048.0D, 0.03125D}, {0.03125D, 2047.0D, 0.03125D}, }; for (int i = 0; i < positions.length; i++) { double[] p = positions[i]; Buf buf = new Buf(); buf.writePosition(p[0], p[1], p[2]); double[] back = new Buf(buf.toByteArray()).readPosition(); eq("position x " + p[0], Double.valueOf(p[0]), Double.valueOf(back[0])); eq("position y " + p[1], Double.valueOf(p[1]), Double.valueOf(back[1])); eq("position z " + p[2], Double.valueOf(p[2]), Double.valueOf(back[2])); } // and the quantisation is a documented property, not an accident Buf fractional = new Buf(); fractional.writePosition(1.0D, 64.5D, 2.0D); double[] cut = new Buf(fractional.toByteArray()).readPosition(); eq("fractional Y rounds to a whole block", Double.valueOf(64.0), Double.valueOf(cut[1])); } private static void testFrame() { byte[] body = new byte[300]; new Random(7).nextBytes(body); Frame frame = new Frame(Packets.HELLO, body); Buf encoded = frame.encode(); Frame back = Frame.decode(encoded, 1024 * 1024); yes("frame came back", back != null); eq("frame id", Integer.valueOf(Packets.HELLO), Integer.valueOf(back.packetId())); eq("frame version", Integer.valueOf(Frame.VERSION), Integer.valueOf(back.version())); yes("frame body", java.util.Arrays.equals(body, back.body())); } private static void testPartialFrame() { Buf encoded = new Frame(Packets.LOGIN, new byte[64]).encode(); byte[] all = encoded.toByteArray(); // a fresh decoder per prefix: the question is whether any prefix short of // the whole frame decodes to nothing, not what happens when prefixes are // concatenated into one stream for (int cut = 0; cut < all.length - 1; cut++) { FrameDecoder decoder = new FrameDecoder(); List got = decoder.offer(all, 0, cut); yes("nothing complete after " + cut + " bytes", got.isEmpty()); } FrameDecoder whole = new FrameDecoder(); for (int cut = 0; cut < all.length; cut++) { whole.offer(all, cut, 1); } eq("exactly one frame once it all arrived", Integer.valueOf(1), Integer.valueOf(whole.framesRead())); } private static void testWrongMagic() { byte[] notOurs = new byte[] {0x7F, 'E', 'L', 'F', 0x01, 0x00, 0x00, 0x00}; FrameDecoder decoder = new FrameDecoder(); refuses("foreign magic", new Runnable() { public void run() { decoder.offer(notOurs, 0, notOurs.length); } }); } private static void testOversizedClaim() { // valid magic and version, but a body of two gigabytes Buf buf = new Buf(); buf.writeInt(Frame.MAGIC); buf.writeVarInt(Frame.VERSION); buf.writeVarInt(Packets.HELLO); buf.writeVarInt(Integer.MAX_VALUE); byte[] bytes = buf.toByteArray(); FrameDecoder decoder = new FrameDecoder(4096); refuses("oversized claim", new Runnable() { public void run() { decoder.offer(bytes, 0, bytes.length); } }); } private static void testSplitEverywhere() { List expected = new ArrayList(); expected.add(new Frame(Packets.HELLO, body("hello"))); expected.add(new Frame(Packets.HELLO_ACK, body("hello-ack"))); expected.add(new Frame(Packets.AUTH, body("auth"))); expected.add(new Frame(Packets.AUTH_ACK, body("auth-ack"))); expected.add(new Frame(Packets.LOGIN, body("login"))); expected.add(new Frame(Packets.LOGIN_ACK, body("login-ack"))); expected.add(new Frame(Packets.DISCONNECT, body("disconnect"))); Buf all = new Buf(); for (int i = 0; i < expected.size(); i++) { all.writeBytes(expected.get(i).encode().toByteArray()); } byte[] wire = all.toByteArray(); FrameDecoder decoder = new FrameDecoder(); List got = new ArrayList(); for (int i = 0; i < wire.length; i++) { got.addAll(decoder.offer(wire, i, 1)); } eq("every frame arrived", Integer.valueOf(expected.size()), Integer.valueOf(got.size())); for (int i = 0; i < expected.size() && i < got.size(); i++) { eq("frame " + i + " id", Integer.valueOf(expected.get(i).packetId()), Integer.valueOf(got.get(i).packetId())); yes("frame " + i + " body", java.util.Arrays.equals(expected.get(i).body(), got.get(i).body())); } eq("nothing left buffered", Integer.valueOf(0), Integer.valueOf(decoder.bufferedBytes())); } private static void testBatch() { FrameDecoder decoder = new FrameDecoder(); List all = new ArrayList(); Buf wire = new Buf(); for (int i = 0; i < 50; i++) { Frame frame = new Frame(Packets.LOGIN, ("frame-" + i).getBytes()); all.add(frame); wire.writeBytes(frame.encode().toByteArray()); } byte[] bytes = wire.toByteArray(); List got = decoder.offer(bytes, 0, bytes.length); eq("all fifty in one read", Integer.valueOf(50), Integer.valueOf(got.size())); eq("counter agrees", Integer.valueOf(50), Integer.valueOf(decoder.framesRead())); for (int i = 0; i < got.size(); i++) { eq("body " + i, all.get(i).body(), got.get(i).body()); } } private static byte[] body(String text) { return new Buf().writeString(text).toByteArray(); } private static void testHandshake() { // the shapes the handshake relies on, exercised through the buffer Buf b = new Buf(); b.writeString("Player"); eq("account name", "Player", new Buf(b.toByteArray()).readString(Packets.MAX_TEXT)); Buf t = new Buf(); t.writeString("proof-token-value"); eq("proof", "proof-token-value", new Buf(t.toByteArray()).readString(Packets.MAX_PROOF)); Buf n = new Buf(); n.writeVarInt(200); n.writeVarInt(500); Buf nr = new Buf(n.toByteArray()); eq("online", Integer.valueOf(200), Integer.valueOf(nr.readVarInt())); eq("max", Integer.valueOf(500), Integer.valueOf(nr.readVarInt())); } /** The play phase packets, over a buffer. */ private static void testPlayPackets() { Packets.JoinWorld join = Packets.JoinWorld.at(120.5D, 64.0D, -300.25D, 90.0F, -10.0F); Packets.JoinWorld joinBack = Packets.JoinWorld.read(new Buf(join.write().toByteArray())); yes("the join carried a position", joinBack.hasPosition); eq("join x", Double.valueOf(120.5D), Double.valueOf(joinBack.x)); eq("join y", Double.valueOf(64.0D), Double.valueOf(joinBack.y)); eq("join z", Double.valueOf(-300.25D), Double.valueOf(joinBack.z)); eq("join yaw", Float.valueOf(90.0F), Float.valueOf(joinBack.yaw)); eq("join pitch", Float.valueOf(-10.0F), Float.valueOf(joinBack.pitch)); Packets.JoinWorld fresh = Packets.JoinWorld.fresh(); Packets.JoinWorld freshBack = Packets.JoinWorld.read(new Buf(fresh.write().toByteArray())); yes("a fresh join carries no position", !freshBack.hasPosition); Packets.WorldJoin world = new Packets.WorldJoin(7, 1234567890123L, 0.5D, 64.0D, 0.5D, 8, 256, Packets.GAMEMODE_CREATIVE); Packets.WorldJoin worldBack = Packets.WorldJoin.read(new Buf(world.write().toByteArray())); eq("entity id", Integer.valueOf(7), Integer.valueOf(worldBack.entityId)); eq("seed", Long.valueOf(1234567890123L), Long.valueOf(worldBack.seed)); eq("view distance", Integer.valueOf(8), Integer.valueOf(worldBack.viewDistance)); eq("world height", Integer.valueOf(256), Integer.valueOf(worldBack.worldHeight)); eq("spawn x", Double.valueOf(0.5D), Double.valueOf(worldBack.spawnX)); eq("game mode", Integer.valueOf(Packets.GAMEMODE_CREATIVE), Integer.valueOf(worldBack.gameMode)); Packets.GameMode mode = new Packets.GameMode(Packets.GAMEMODE_SURVIVAL); Packets.GameMode modeBack = Packets.GameMode.read(new Buf(mode.write().toByteArray())); eq("survival comes back", Integer.valueOf(Packets.GAMEMODE_SURVIVAL), Integer.valueOf(modeBack.mode)); yes("survival cannot break blocks", !modeBack.creative()); yes("creative can break blocks", new Packets.GameMode(Packets.GAMEMODE_CREATIVE).creative()); Packets.BlockChange change = new Packets.BlockChange(-100, 70, 250, Blocks.STONE); Packets.BlockChange changeBack = Packets.BlockChange.read(new Buf(change.write().toByteArray())); eq("block x", Integer.valueOf(-100), Integer.valueOf(changeBack.x)); eq("block y", Integer.valueOf(70), Integer.valueOf(changeBack.y)); eq("block z", Integer.valueOf(250), Integer.valueOf(changeBack.z)); eq("block id", Byte.valueOf(Blocks.STONE), Byte.valueOf(changeBack.blockId)); Packets.PlayerMove move = new Packets.PlayerMove(1.5D, 64.0D, -2.25D, 45.0F, -45.0F, true); Packets.PlayerMove moveBack = Packets.PlayerMove.read(new Buf(move.write().toByteArray())); eq("move x", Double.valueOf(1.5D), Double.valueOf(moveBack.x)); eq("move z", Double.valueOf(-2.25D), Double.valueOf(moveBack.z)); eq("move yaw", Float.valueOf(45.0F), Float.valueOf(moveBack.yaw)); yes("the player is on the ground", moveBack.onGround); Packets.EntityMove entity = new Packets.EntityMove(9, 8.0D, 9.0D, 10.0D, 1.0F, 2.0F); Packets.EntityMove entityBack = Packets.EntityMove.read(new Buf(entity.write().toByteArray())); eq("entity id", Integer.valueOf(9), Integer.valueOf(entityBack.entityId)); eq("entity x", Double.valueOf(8.0D), Double.valueOf(entityBack.x)); eq("entity z", Double.valueOf(10.0D), Double.valueOf(entityBack.z)); Packets.Ping ping = new Packets.Ping(1234567890123L); eq("ping", Long.valueOf(1234567890123L), Long.valueOf(Packets.Ping.read(new Buf(ping.write().toByteArray())).id)); Packets.Pong pong = new Packets.Pong(99L); eq("pong", Long.valueOf(99L), Long.valueOf(Packets.Pong.read(new Buf(pong.write().toByteArray())).id)); Packets.EntitySpawn spawn = new Packets.EntitySpawn(4, 4, "Player", 1.0D, 65.0D, 2.0D, 30.0F, -60.0F); Packets.EntitySpawn spawnBack = Packets.EntitySpawn.read(new Buf(spawn.write().toByteArray())); eq("spawn entity id", Integer.valueOf(4), Integer.valueOf(spawnBack.entityId)); eq("spawn player id", Integer.valueOf(4), Integer.valueOf(spawnBack.playerId)); eq("spawn name", "Player", spawnBack.name); eq("spawn y", Double.valueOf(65.0D), Double.valueOf(spawnBack.y)); Packets.EntityRemove remove = new Packets.EntityRemove(11); eq("remove", Integer.valueOf(11), Integer.valueOf( Packets.EntityRemove.read(new Buf(remove.write().toByteArray())).entityId)); Packets.LauncherAuth launcher = new Packets.LauncherAuth("Player", "00000000-0000-0000-0000-000000000000", "a.b.c"); Packets.LauncherAuth launcherBack = Packets.LauncherAuth.read(new Buf(launcher.write().toByteArray())); eq("launcher account", "Player", launcherBack.account); eq("launcher uuid", "00000000-0000-0000-0000-000000000000", launcherBack.uuid); eq("launcher token", "a.b.c", launcherBack.token); } private static void testChunkCompression() { byte[] raw = new byte[ChunkFormat.VOLUME]; for (int z = 0; z < ChunkFormat.WIDTH; z++) { for (int x = 0; x < ChunkFormat.WIDTH; x++) { for (int y = 0; y < 64; y++) { raw[ChunkFormat.index(x, y, z)] = y == 63 ? Blocks.GRASS : Blocks.STONE; } } } byte[] packed = ChunkFormat.compress(raw); yes("a chunk of mostly air is small (" + packed.length + " bytes)", packed.length < 4096); eq("the chunk came back", raw, ChunkFormat.decompress(packed, ChunkFormat.VOLUME)); Packets.ChunkData data = new Packets.ChunkData(-3, 9, raw); Packets.ChunkData back = Packets.ChunkData.read(new Buf(data.write().toByteArray())); eq("chunk x", Integer.valueOf(-3), Integer.valueOf(back.chunkX)); eq("chunk z", Integer.valueOf(9), Integer.valueOf(back.chunkZ)); eq("the blocks came back", raw, back.blocks); } private static void testBadChunkData() { // 0xFF is not a valid deflate block header, so this cannot decode final byte[] rubbish = new byte[64]; java.util.Arrays.fill(rubbish, (byte) 0xFF); refuses("a chunk that is not compressed data", new Runnable() { public void run() { ChunkFormat.decompress(rubbish, ChunkFormat.VOLUME); } }); } private static void testShortChunk() { byte[] raw = new byte[ChunkFormat.VOLUME]; byte[] packed = ChunkFormat.compress(raw); final byte[] cut = java.util.Arrays.copyOf(packed, packed.length / 2); refuses("a truncated chunk", new Runnable() { public void run() { ChunkFormat.decompress(cut, ChunkFormat.VOLUME); } }); } private static void testPacketNames() { java.util.Map names = Packets.names(); int[] ids = { Packets.HELLO, Packets.HELLO_ACK, Packets.AUTH, Packets.AUTH_ACK, Packets.LOGIN, Packets.LOGIN_ACK, Packets.JOIN_WORLD, Packets.WORLD_JOIN, Packets.CHUNK_DATA, Packets.BLOCK_CHANGE, Packets.PLAYER_MOVE, Packets.ENTITY_MOVE, Packets.PING, Packets.PONG, Packets.ENTITY_SPAWN, Packets.ENTITY_REMOVE, Packets.LAUNCHER_AUTH, Packets.SET_GAME_MODE, Packets.INVENTORY, Packets.INVENTORY_SLOT, Packets.DIG, Packets.BREAK_PROGRESS, Packets.CRAFT_START, Packets.CRAFT_UPDATE, Packets.RESEARCH_START, Packets.RESEARCH_UPDATE, Packets.TECH_TREE_UNLOCK, Packets.KNOWN_RECIPES, Packets.TECH_TREE_STATE, Packets.CONTAINER_OPEN, Packets.CONTAINER_TAKE, Packets.LEARN_BLUEPRINT, Packets.DISCONNECT, }; for (int i = 0; i < ids.length; i++) { yes("packet " + ids[i] + " has a name", names.containsKey(Integer.valueOf(ids[i]))); } eq("no id is used twice", Integer.valueOf(ids.length), Integer.valueOf(names.size())); } private static void testItems() { // The numbers the game is built on. If these move, the game stops // matching its own rules, so they are pinned here. ItemType rock = ItemRegistry.require(ItemRegistry.ROCK); eq("the rock is called a rock", "Rock", rock.name); eq("the rock hits the head for twenty", Integer.valueOf(20), Integer.valueOf(rock.damage(ItemType.HEAD))); eq("and the torso for ten", Integer.valueOf(10), Integer.valueOf(rock.damage(ItemType.TORSO))); eq("the rock takes fifty off a tree", Integer.valueOf(50), Integer.valueOf(rock.gather(ResourceClass.TREE).damage)); eq("and thirty five off an ore", Integer.valueOf(35), Integer.valueOf(rock.gather(ResourceClass.METAL).damage)); eq("the rock is known from the start", Boolean.TRUE, Boolean.valueOf(rock.knownByDefault)); eq("the rock needs no workbench", Integer.valueOf(0), Integer.valueOf(rock.bench)); yes("the rock has a recipe", rock.recipe != null); eq("the rock is made of ten stones", Integer.valueOf(10), Integer.valueOf(rock.recipe.needs(ItemRegistry.STONE))); ItemType hatchet = ItemRegistry.require(ItemRegistry.STONE_HATCHET); eq("the stone hatchet is an axe", ToolClass.AXE, hatchet.tool); eq("it takes eighty off a tree", Integer.valueOf(80), Integer.valueOf(hatchet.gather(ResourceClass.TREE).damage)); ItemType pickaxe = ItemRegistry.require(ItemRegistry.STONE_PICKAXE); eq("the stone pickaxe is a pickaxe", ToolClass.PICKAXE, pickaxe.tool); eq("it takes eighty off an ore", Integer.valueOf(80), Integer.valueOf(pickaxe.gather(ResourceClass.METAL).damage)); yes("and it does not cut trees", pickaxe.gather(ResourceClass.TREE) == null); ItemType sword = ItemRegistry.require(ItemRegistry.SALVAGED_SWORD); eq("the sword is a sword", ToolClass.SWORD, sword.tool); yes("the sword gathers nothing but flesh", sword.gather(ResourceClass.TREE) == null && sword.gather(ResourceClass.METAL) == null && sword.gather(ResourceClass.FLESH) != null); ItemType workbench2 = ItemRegistry.require(ItemRegistry.WORKBENCH_2); eq("the second workbench needs the first", Integer.valueOf(1), Integer.valueOf(workbench2.bench)); ItemType workbench3 = ItemRegistry.require(ItemRegistry.WORKBENCH_3); eq("the third needs the second", Integer.valueOf(2), Integer.valueOf(workbench3.bench)); // the resources are gathered, not made ItemType wood = ItemRegistry.require(ItemRegistry.WOOD); yes("wood has no recipe", wood.recipe == null); yes("every id is different", ItemRegistry.all().size() > 20); } private static void testInventory() { Packets.Slot[] slots = new Packets.Slot[6]; slots[0] = new Packets.Slot(ItemRegistry.ROCK, 1, 100); slots[1] = new Packets.Slot(ItemRegistry.WOOD, 12, -1); slots[2] = new Packets.Slot(ItemRegistry.NOTHING, 0, -1); Packets.Inventory inventory = new Packets.Inventory(slots, 0); Packets.Inventory back = Packets.Inventory.read(new Buf(inventory.write().toByteArray())); eq("the slots survive", Integer.valueOf(6), Integer.valueOf(back.slots.length)); eq("the rock is in the first slot", Integer.valueOf(ItemRegistry.ROCK), Integer.valueOf(back.slots[0].item)); eq("with one of it", Integer.valueOf(1), Integer.valueOf(back.slots[0].count)); eq("and its life", Integer.valueOf(100), Integer.valueOf(back.slots[0].condition)); eq("wood is in the second", Integer.valueOf(12), Integer.valueOf(back.slots[1].count)); eq("an empty slot stays empty", Integer.valueOf(ItemRegistry.NOTHING), Integer.valueOf(back.slots[2].item)); eq("the hand survives", Integer.valueOf(0), Integer.valueOf(back.held)); Packets.SlotChange change = new Packets.SlotChange(3, ItemRegistry.SCRAP, 7, -1); Packets.SlotChange changeBack = Packets.SlotChange.read(new Buf(change.write().toByteArray())); eq("the slot number survives", Integer.valueOf(3), Integer.valueOf(changeBack.slot)); eq("the item survives", Integer.valueOf(ItemRegistry.SCRAP), Integer.valueOf(changeBack.item)); eq("the count survives", Integer.valueOf(7), Integer.valueOf(changeBack.count)); // a stack carries the data that belongs to it ItemStack stack = ItemStack.of(ItemRegistry.ROCK, 1); stack.set("charged", "yes"); eq("the data travels", "yes", stack.copy().get("charged")); eq("the copy is separate", Boolean.FALSE, Boolean.valueOf(stack == stack.copy())); } private static void testLyingLength() { // announces a large but legal body, sends nothing, and keeps quiet. // The decoder must hold it without growing and without pretending the // frame arrived. FrameDecoder decoder = new FrameDecoder(64 * 1024); Buf header = new Buf(); header.writeInt(Frame.MAGIC); header.writeVarInt(Frame.VERSION); header.writeVarInt(Packets.HELLO); header.writeVarInt(60 * 1024); byte[] bytes = header.toByteArray(); List got = decoder.offer(bytes, 0, bytes.length); yes("no frame yet", got.isEmpty()); yes("held the header", decoder.bufferedBytes() == bytes.length); // and the rest arrives, so it completes byte[] body = new byte[60 * 1024]; got = decoder.offer(body, 0, body.length); eq("completes once the body arrives", Integer.valueOf(1), Integer.valueOf(got.size())); } }