d2a0a7a93b
Аутентификация переделана по схеме пользователя: лаунчер — единственный
источник токена, сервер проверяет его сам и лаунчера не спрашивает.
- rsx-auth/GameToken: токен это base64url(payload) + "." + base64url(HMAC-SHA256),
payload {uuid, nick, aud, exp, jti, typ}, typ = session или reload.
- Сессия живёт час, reload-токен полтора часа и только внутри игры. Пакет
SessionReload продлевает сессию и выдаёт новый reload-токен.
- Сервер проверяет подпись, аудиенс, срок и совпадение uuid. HTTP-запрос к
лаунчеру из сервера убран.
- Клиент: окно пароля убрано совсем, ник берётся из токена, клиент сам
продлевает сессию за 5 минут до конца.
- Для локальной разработки сервер с --dev-token пишет свой токен в
world/dev-token.txt, клиент берёт токен оттуда.
- Права админа: свой список world/admins.txt, перечитывается раз в 5 секунд,
поэтому право снимается на лету. Пакет AdminState.
- Ванильные блоки мира ломает только админ, режим игры прав не даёт.
- F3+B только для админов: без прав показываем отказ, с правом включаются и
ванильные хитбоксы, и наш отладочный рендер. Права сняли посреди просмотра —
гаснут сразу.
- run-game.ps1 умеет -Token.
Проверено на живом сервере: own session ok, player in, rights of Player given.
Тесты: протокол 34, сервер 88, форк собирается чисто.
679 lines
30 KiB
Java
679 lines
30 KiB
Java
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<String> failures = new ArrayList<String>();
|
|
|
|
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<Frame> 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<Frame> expected = new ArrayList<Frame>();
|
|
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<Frame> got = new ArrayList<Frame>();
|
|
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<Frame> all = new ArrayList<Frame>();
|
|
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<Frame> 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.TokenAuth launcher = new Packets.TokenAuth("Player",
|
|
"00000000-0000-0000-0000-000000000000", "a.b.c");
|
|
Packets.TokenAuth launcherBack =
|
|
Packets.TokenAuth.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<Integer, String> 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.TOKEN_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.HITBOXES, Packets.HITBOX_INSTANCES, Packets.HITBOXES_RELOAD,
|
|
Packets.ATTACK, Packets.CHAT, Packets.CHAT_SEND, Packets.PLAYER_LIST,
|
|
Packets.SESSION_RELOAD, Packets.SESSION_RENEWED, Packets.ADMIN_STATE,
|
|
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<Frame> 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()));
|
|
}
|
|
} |