Словарь протокола: блоки, формат чанка и игровые пакеты
Протокол до сих пор умел только рукопожатие: поздороваться, доказать пароль, получить токен и войти. Дальше играть было нечем. Добавлено: - Packet, интерфейс, который умеет написать себя сам. Раньше отправитель разбирал тип пакета в ветке instanceof, и с каждым новым видом пакета эта ветка росла. Теперь отправитель знает только, что у пакета есть write(). - Блоки и ChunkFormat переехали в протокол. Идентификаторы блоков и геометрия чанка — это словарь провода, а не внутреннее дело сервера: клиент получает чанк байт на блок и обязан знать, что означает каждый байт. Один источник правды вместо двух, которые могут разойтись. - Игровые пакеты: JoinWorld, WorldJoin, ChunkData, BlockChange, PlayerMove, EntityMove, Ping/Pong, EntitySpawn, EntityRemove. - Чанк уходит по проводу сжатым. Чанк это 65536 байт, и почти все они это воздух, поэтому дефлейт тут не украшение, а разница между «сервер принял» и «клиент вошёл». - Имена всем пакетам в таблице names(), чтобы забытый пакет был виден сразу, а не всплывал через месяц. Проверено: 22 теста протокола, включая круг чанка через сжатие, отказ битому сжатому блоку и отказ обрезанному.
This commit is contained in:
@@ -67,6 +67,21 @@ public final class ProtocolTest {
|
||||
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("a lying length cannot stall the heap", new Runnable() {
|
||||
public void run() { testLyingLength(); }
|
||||
});
|
||||
@@ -384,6 +399,137 @@ public final class ProtocolTest {
|
||||
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.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));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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.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 testLyingLength() {
|
||||
// announces a large but legal body, sends nothing, and keeps quiet.
|
||||
// The decoder must hold it without growing and without pretending the
|
||||
|
||||
Reference in New Issue
Block a user