From 93978378f690ca46d3373f62818d99914933802d Mon Sep 17 00:00:00 2001 From: Steven Smith Date: Sat, 22 Aug 2015 22:25:25 -0700 Subject: [PATCH] Update chunk sending protocol. --- .../spacehq/mc/protocol/data/game/Chunk.java | 63 -------- .../mc/protocol/data/game/EntityMetadata.java | 19 +-- .../mc/protocol/data/game/ItemStack.java | 22 +-- .../mc/protocol/data/game/Position.java | 19 +-- .../mc/protocol/data/game/Rotation.java | 18 +-- .../mc/protocol/data/game/ShortArray3d.java | 70 -------- .../data/game/chunk/BlockStateMap.java | 91 +++++++++++ .../data/game/chunk/BlockStorage.java | 78 +++++++++ .../mc/protocol/data/game/chunk/Chunk.java | 46 ++++++ .../mc/protocol/data/game/chunk/Column.java | 69 ++++++++ .../data/game/chunk/FlexibleStorage.java | 121 ++++++++++++++ .../data/game/{ => chunk}/NibbleArray3d.java | 27 ++-- .../server/world/ServerChunkDataPacket.java | 107 +++---------- .../world/ServerMultiChunkDataPacket.java | 127 +++++---------- .../org/spacehq/mc/protocol/util/NetUtil.java | 151 +++++------------- .../mc/protocol/util/NetworkChunkData.java | 55 ------- .../mc/protocol/util/ParsedChunkData.java | 45 ------ 17 files changed, 549 insertions(+), 579 deletions(-) delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/Chunk.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/ShortArray3d.java create mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStateMap.java create mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java create mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java create mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java create mode 100644 src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java rename src/main/java/org/spacehq/mc/protocol/data/game/{ => chunk}/NibbleArray3d.java (72%) delete mode 100644 src/main/java/org/spacehq/mc/protocol/util/NetworkChunkData.java delete mode 100644 src/main/java/org/spacehq/mc/protocol/util/ParsedChunkData.java diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/Chunk.java b/src/main/java/org/spacehq/mc/protocol/data/game/Chunk.java deleted file mode 100644 index 52bd8d86..00000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/Chunk.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.spacehq.mc.protocol.data.game; - -public class Chunk { - - private ShortArray3d blocks; - private NibbleArray3d blocklight; - private NibbleArray3d skylight; - - public Chunk(boolean skylight) { - this(new ShortArray3d(4096), new NibbleArray3d(4096), skylight ? new NibbleArray3d(4096) : null); - } - - public Chunk(ShortArray3d blocks, NibbleArray3d blocklight, NibbleArray3d skylight) { - this.blocks = blocks; - this.blocklight = blocklight; - this.skylight = skylight; - } - - public ShortArray3d getBlocks() { - return this.blocks; - } - - public NibbleArray3d getBlockLight() { - return this.blocklight; - } - - public NibbleArray3d getSkyLight() { - return this.skylight; - } - - public boolean isEmpty() { - for(short block : this.blocks.getData()) { - if(block != 0) { - return false; - } - } - - return true; - } - - @Override - public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - Chunk chunk = (Chunk) o; - - if(!blocklight.equals(chunk.blocklight)) return false; - if(!blocks.equals(chunk.blocks)) return false; - if(skylight != null ? !skylight.equals(chunk.skylight) : chunk.skylight != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = blocks.hashCode(); - result = 31 * result + blocklight.hashCode(); - result = 31 * result + (skylight != null ? skylight.hashCode() : 0); - return result; - } - -} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/EntityMetadata.java b/src/main/java/org/spacehq/mc/protocol/data/game/EntityMetadata.java index 405fce75..36193645 100644 --- a/src/main/java/org/spacehq/mc/protocol/data/game/EntityMetadata.java +++ b/src/main/java/org/spacehq/mc/protocol/data/game/EntityMetadata.java @@ -3,7 +3,6 @@ package org.spacehq.mc.protocol.data.game; import org.spacehq.mc.protocol.data.game.values.entity.MetadataType; public class EntityMetadata { - private int id; private MetadataType type; private Object value; @@ -28,24 +27,14 @@ public class EntityMetadata { @Override public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - EntityMetadata metadata = (EntityMetadata) o; - - if(id != metadata.id) return false; - if(type != metadata.type) return false; - if(!value.equals(metadata.value)) return false; - - return true; + return this == o || (o instanceof EntityMetadata && this.id == ((EntityMetadata) o).id && this.type == ((EntityMetadata) o).type && this.value.equals(((EntityMetadata) o).value)); } @Override public int hashCode() { - int result = id; - result = 31 * result + type.hashCode(); - result = 31 * result + value.hashCode(); + int result = this.id; + result = 31 * result + this.type.hashCode(); + result = 31 * result + this.value.hashCode(); return result; } - } diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/ItemStack.java b/src/main/java/org/spacehq/mc/protocol/data/game/ItemStack.java index ab2734eb..90f49ef4 100644 --- a/src/main/java/org/spacehq/mc/protocol/data/game/ItemStack.java +++ b/src/main/java/org/spacehq/mc/protocol/data/game/ItemStack.java @@ -3,7 +3,6 @@ package org.spacehq.mc.protocol.data.game; import org.spacehq.opennbt.tag.builtin.CompoundTag; public class ItemStack { - private int id; private int amount; private int data; @@ -46,26 +45,15 @@ public class ItemStack { @Override public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - ItemStack itemStack = (ItemStack) o; - - if(amount != itemStack.amount) return false; - if(data != itemStack.data) return false; - if(id != itemStack.id) return false; - if(nbt != null ? !nbt.equals(itemStack.nbt) : itemStack.nbt != null) return false; - - return true; + return this == o || (o instanceof ItemStack && this.id == ((ItemStack) o).id && this.amount == ((ItemStack) o).amount && this.data == ((ItemStack) o).data && ((this.nbt != null && ((ItemStack) o).nbt != null) || (this.nbt != null && this.nbt.equals(((ItemStack) o).nbt)))); } @Override public int hashCode() { - int result = id; - result = 31 * result + amount; - result = 31 * result + data; - result = 31 * result + (nbt != null ? nbt.hashCode() : 0); + int result = this.id; + result = 31 * result + this.amount; + result = 31 * result + this.data; + result = 31 * result + (this.nbt != null ? this.nbt.hashCode() : 0); return result; } - } diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/Position.java b/src/main/java/org/spacehq/mc/protocol/data/game/Position.java index 37a1dce4..01fb2f5c 100644 --- a/src/main/java/org/spacehq/mc/protocol/data/game/Position.java +++ b/src/main/java/org/spacehq/mc/protocol/data/game/Position.java @@ -1,7 +1,6 @@ package org.spacehq.mc.protocol.data.game; public class Position { - private int x; private int y; private int z; @@ -26,24 +25,14 @@ public class Position { @Override public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - Position position = (Position) o; - - if(x != position.x) return false; - if(y != position.y) return false; - if(z != position.z) return false; - - return true; + return this == o || (o instanceof Position && this.x == ((Position) o).x && this.y == ((Position) o).y && this.z == ((Position) o).z); } @Override public int hashCode() { - int result = x; - result = 31 * result + y; - result = 31 * result + z; + int result = this.x; + result = 31 * result + this.y; + result = 31 * result + this.z; return result; } - } diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/Rotation.java b/src/main/java/org/spacehq/mc/protocol/data/game/Rotation.java index 533f90ec..4b82a748 100644 --- a/src/main/java/org/spacehq/mc/protocol/data/game/Rotation.java +++ b/src/main/java/org/spacehq/mc/protocol/data/game/Rotation.java @@ -29,24 +29,14 @@ public class Rotation { @Override public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - Rotation rotation = (Rotation) o; - - if(Float.compare(rotation.pitch, pitch) != 0) return false; - if(Float.compare(rotation.roll, roll) != 0) return false; - if(Float.compare(rotation.yaw, yaw) != 0) return false; - - return true; + return this == o || (o instanceof Rotation && Float.compare(this.pitch, ((Rotation) o).pitch) == 0 && Float.compare(this.yaw, ((Rotation) o).yaw) == 0 && Float.compare(this.roll, ((Rotation) o).roll) == 0); } @Override public int hashCode() { - int result = (pitch != +0.0f ? Float.floatToIntBits(pitch) : 0); - result = 31 * result + (yaw != +0.0f ? Float.floatToIntBits(yaw) : 0); - result = 31 * result + (roll != +0.0f ? Float.floatToIntBits(roll) : 0); + int result = Float.hashCode(this.pitch); + result = 31 * result + Float.hashCode(this.yaw); + result = 31 * result + Float.hashCode(this.roll); return result; } - } diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/ShortArray3d.java b/src/main/java/org/spacehq/mc/protocol/data/game/ShortArray3d.java deleted file mode 100644 index 7d50248c..00000000 --- a/src/main/java/org/spacehq/mc/protocol/data/game/ShortArray3d.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.spacehq.mc.protocol.data.game; - -import java.util.Arrays; - -public class ShortArray3d { - - private short[] data; - - public ShortArray3d(int size) { - this.data = new short[size]; - } - - public ShortArray3d(short[] array) { - this.data = array; - } - - public short[] getData() { - return this.data; - } - - public int get(int x, int y, int z) { - return this.data[y << 8 | z << 4 | x] & 0xFFFF; - } - - public void set(int x, int y, int z, int val) { - this.data[y << 8 | z << 4 | x] = (short) val; - } - - public int getBlock(int x, int y, int z) { - return this.get(x, y, z) >> 4; - } - - public void setBlock(int x, int y, int z, int block) { - this.set(x, y, z, block << 4 | this.getData(x, y, z)); - } - - public int getData(int x, int y, int z) { - return this.get(x, y, z) & 0xF; - } - - public void setData(int x, int y, int z, int data) { - this.set(x, y, z, this.getBlock(x, y, z) << 4 | data); - } - - public void setBlockAndData(int x, int y, int z, int block, int data) { - this.set(x, y, z, block << 4 | data); - } - - public void fill(int val) { - Arrays.fill(this.data, (short) val); - } - - @Override - public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - ShortArray3d that = (ShortArray3d) o; - - if(!Arrays.equals(data, that.data)) return false; - - return true; - } - - @Override - public int hashCode() { - return Arrays.hashCode(data); - } - -} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStateMap.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStateMap.java new file mode 100644 index 00000000..d3114020 --- /dev/null +++ b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStateMap.java @@ -0,0 +1,91 @@ +package org.spacehq.mc.protocol.data.game.chunk; + +import org.spacehq.packetlib.io.NetInput; +import org.spacehq.packetlib.io.NetOutput; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class BlockStateMap { + private static final int[] MAX_ID_PER_BYTES = { 15, 255, 4095 }; + + private BlockStorage parent; + + private Map idToState = new HashMap(); + private Map stateToId = new HashMap(); + private int stateCount = 0; + + private int currentSize = 0; + + public BlockStateMap(BlockStorage parent) { + this.parent = parent; + + this.idToState.put(0, 0); + this.stateToId.put(0, 0); + this.stateCount++; + } + + public BlockStateMap(BlockStorage parent, NetInput in) throws IOException { + this.parent = parent; + + this.stateCount = in.readVarInt(); + for(int i = 0; i < this.stateCount; i++) { + int state = in.readVarInt(); + int id = in.readVarInt(); + + this.idToState.put(id, state); + this.stateToId.put(state, id); + } + } + + public void write(NetOutput out) throws IOException { + out.writeVarInt(this.stateCount); + for(Map.Entry entry : this.stateToId.entrySet()) { + out.writeVarInt(entry.getKey()); + out.writeVarInt(entry.getValue()); + } + } + + public Map getIdsToStates() { + return new HashMap(this.idToState); + } + + public Map getStatesToIds() { + return new HashMap(this.stateToId); + } + + public int getState(int id) { + return this.idToState.containsKey(id) ? this.idToState.get(id) : 0; + } + + public int getId(int state) { + if(!this.stateToId.containsKey(state)) { + int id = this.stateCount; + this.idToState.put(id, state); + this.stateToId.put(state, id); + + if(this.currentSize < MAX_ID_PER_BYTES.length - 1 && this.stateCount > MAX_ID_PER_BYTES[this.currentSize]) { + this.parent.resize(MAX_ID_PER_BYTES[++this.currentSize]); + } + + this.stateCount++; + } + + return this.stateToId.get(state); + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof BlockStateMap && this.idToState.equals(((BlockStateMap) o).idToState) && this.stateToId.equals(((BlockStateMap) o).stateToId) && this.stateCount == ((BlockStateMap) o).stateCount && this.currentSize == ((BlockStateMap) o).currentSize); + } + + @Override + public int hashCode() { + int result = this.idToState.hashCode(); + result = 31 * result + this.stateToId.hashCode(); + result = 31 * result + this.stateCount; + result = 31 * result + this.currentSize; + return result; + } +} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java new file mode 100644 index 00000000..0bf0615e --- /dev/null +++ b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/BlockStorage.java @@ -0,0 +1,78 @@ +package org.spacehq.mc.protocol.data.game.chunk; + +import org.spacehq.mc.protocol.util.NetUtil; +import org.spacehq.packetlib.io.NetInput; +import org.spacehq.packetlib.io.NetOutput; + +import java.io.IOException; + +public class BlockStorage { + private BlockStateMap stateMap; + private FlexibleStorage storage; + + public BlockStorage() { + this.stateMap = new BlockStateMap(this); + this.storage = new FlexibleStorage(4, 4096); + } + + public BlockStorage(NetInput in) throws IOException { + this.stateMap = new BlockStateMap(this, in); + this.storage = new FlexibleStorage(in); + } + + public void write(NetOutput out) throws IOException { + this.stateMap.write(out); + this.storage.write(out); + } + + public BlockStateMap getStateMap() { + return this.stateMap; + } + + public FlexibleStorage getStorage() { + return this.storage; + } + + public int get(int x, int y, int z) { + return this.stateMap.getState(this.storage.get(index(x, y, z))); + } + + public void set(int x, int y, int z, int state) { + int id = this.stateMap.getId(state); + this.storage.set(index(x, y, z), id); + } + + public boolean isEmpty() { + for(int index = 0; index < this.storage.getSize(); index++) { + if(this.storage.get(index) != 0) { + return true; + } + } + + return false; + } + + protected void resize(int size) { + FlexibleStorage oldStorage = this.storage; + this.storage = new FlexibleStorage(NetUtil.nextExponentOfTwo(size), this.storage.getSize()); + for(int index = 0; index < this.storage.getSize(); index++) { + this.storage.set(index, oldStorage.get(index)); + } + } + + private static int index(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof BlockStorage && this.stateMap.equals(((BlockStorage) o).stateMap) && this.storage.equals(((BlockStorage) o).storage)); + } + + @Override + public int hashCode() { + int result = this.stateMap.hashCode(); + result = 31 * result + this.storage.hashCode(); + return result; + } +} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java new file mode 100644 index 00000000..a3160c6d --- /dev/null +++ b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Chunk.java @@ -0,0 +1,46 @@ +package org.spacehq.mc.protocol.data.game.chunk; + +public class Chunk { + private BlockStorage blocks; + private NibbleArray3d blocklight; + private NibbleArray3d skylight; + + public Chunk(boolean skylight) { + this(new BlockStorage(), new NibbleArray3d(4096), skylight ? new NibbleArray3d(4096) : null); + } + + public Chunk(BlockStorage blocks, NibbleArray3d blocklight, NibbleArray3d skylight) { + this.blocks = blocks; + this.blocklight = blocklight; + this.skylight = skylight; + } + + public BlockStorage getBlocks() { + return this.blocks; + } + + public NibbleArray3d getBlockLight() { + return this.blocklight; + } + + public NibbleArray3d getSkyLight() { + return this.skylight; + } + + public boolean isEmpty() { + return this.blocks.isEmpty(); + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof Chunk && this.blocks.equals(((Chunk) o).blocks) && this.blocklight.equals(((Chunk) o).blocklight) && ((this.skylight == null && (((Chunk) o).skylight == null)) || (this.skylight != null && this.skylight.equals(((Chunk) o).skylight)))); + } + + @Override + public int hashCode() { + int result = this.blocks.hashCode(); + result = 31 * result + this.blocklight.hashCode(); + result = 31 * result + (this.skylight != null ? this.skylight.hashCode() : 0); + return result; + } +} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java new file mode 100644 index 00000000..ce604702 --- /dev/null +++ b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/Column.java @@ -0,0 +1,69 @@ +package org.spacehq.mc.protocol.data.game.chunk; + +public class Column { + private int x; + private int z; + private Chunk chunks[]; + private byte biomeData[]; + + private boolean skylight; + + public Column(int x, int z, Chunk chunks[]) { + this(x, z, chunks, null); + } + + public Column(int x, int z, Chunk chunks[], byte biomeData[]) { + if(chunks.length != 16) { + throw new IllegalArgumentException("Chunk array length must be 16."); + } + + if(biomeData != null && biomeData.length != 256) { + throw new IllegalArgumentException("Biome data array length must be 256."); + } + + this.skylight = false; + boolean noSkylight = false; + for(int index = 0; index < chunks.length; index++) { + if(chunks[index] != null) { + if(chunks[index].getSkyLight() == null) { + noSkylight = true; + } else { + this.skylight = true; + } + } + } + + if(noSkylight && this.skylight) { + throw new IllegalArgumentException("Either all chunks must have skylight values or none must have them."); + } + + this.x = x; + this.z = z; + this.chunks = chunks; + this.biomeData = biomeData; + } + + public int getX() { + return this.x; + } + + public int getZ() { + return this.z; + } + + public Chunk[] getChunks() { + return this.chunks; + } + + public boolean hasBiomeData() { + return this.biomeData != null; + } + + public byte[] getBiomeData() { + return this.biomeData; + } + + public boolean hasSkylight() { + return this.skylight; + } +} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java new file mode 100644 index 00000000..de82e2ae --- /dev/null +++ b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/FlexibleStorage.java @@ -0,0 +1,121 @@ +package org.spacehq.mc.protocol.data.game.chunk; + +import org.spacehq.packetlib.io.NetInput; +import org.spacehq.packetlib.io.NetOutput; + +import java.io.IOException; +import java.util.Arrays; + +public class FlexibleStorage { + private final long data[]; + private final int bitsPerEntry; + private final int size; + private final long maxEntryValue; + + public FlexibleStorage(int bitsPerEntry, int size) { + if(bitsPerEntry < 1 || bitsPerEntry > 32) { + throw new IllegalArgumentException("BitsPerEntry cannot be outside of accepted range."); + } + + this.bitsPerEntry = bitsPerEntry; + this.size = size; + + this.maxEntryValue = (1L << bitsPerEntry) - 1; + this.data = new long[roundToNearest(size * bitsPerEntry, 64) / 64]; + } + + public FlexibleStorage(NetInput in) throws IOException { + this.bitsPerEntry = in.readVarInt(); + if(this.bitsPerEntry < 1 || this.bitsPerEntry > 32) { + throw new IllegalArgumentException("BitsPerEntry cannot be outside of accepted range."); + } + + this.data = in.readLongs(in.readVarInt()); + this.size = this.data.length * 64 / this.bitsPerEntry; + this.maxEntryValue = (1L << this.bitsPerEntry) - 1; + } + + public void write(NetOutput out) throws IOException { + out.writeVarInt(this.bitsPerEntry); + out.writeVarInt(this.data.length); + out.writeLongs(this.data); + } + + public long[] getData() { + return this.data; + } + + public int getBitsPerEntry() { + return this.bitsPerEntry; + } + + public int getSize() { + return this.size; + } + + public int get(int index) { + if(index < 0 || index > this.size - 1) { + throw new IndexOutOfBoundsException(); + } + + int bitIndex = index * this.bitsPerEntry; + int startIndex = bitIndex / 64; + int endIndex = ((index + 1) * this.bitsPerEntry - 1) / 64; + int startBitSubIndex = bitIndex % 64; + if(startIndex == endIndex) { + return (int) (this.data[startIndex] >>> startBitSubIndex & this.maxEntryValue); + } else { + int endBitSubIndex = 64 - startBitSubIndex; + return (int) ((this.data[startIndex] >>> startBitSubIndex | this.data[endIndex] << endBitSubIndex) & this.maxEntryValue); + } + } + + public void set(int index, int value) { + if(index < 0 || index > this.size - 1) { + throw new IndexOutOfBoundsException(); + } + + if(value < 0 || value > this.maxEntryValue) { + throw new IllegalArgumentException("Value cannot be outside of accepted range."); + } + + int bitIndex = index * this.bitsPerEntry; + int startIndex = bitIndex / 64; + int endIndex = ((index + 1) * this.bitsPerEntry - 1) / 64; + int startBitSubIndex = bitIndex % 64; + this.data[startIndex] = this.data[startIndex] & ~(this.maxEntryValue << startBitSubIndex) | ((long) value & this.maxEntryValue) << startBitSubIndex; + if(startIndex != endIndex) { + int endBitSubIndex = 64 - startBitSubIndex; + this.data[endIndex] = this.data[endIndex] >>> endBitSubIndex << endBitSubIndex | ((long) value & this.maxEntryValue) >> endBitSubIndex; + } + } + + private static int roundToNearest(int value, int roundTo) { + if(roundTo == 0) { + return 0; + } else if(value == 0) { + return roundTo; + } else { + if(value < 0) { + roundTo *= -1; + } + + int remainder = value % roundTo; + return remainder != 0 ? value + roundTo - remainder : value; + } + } + + @Override + public boolean equals(Object o) { + return this == o || (o instanceof FlexibleStorage && Arrays.equals(this.data, ((FlexibleStorage) o).data) && this.bitsPerEntry == ((FlexibleStorage) o).bitsPerEntry && this.size == ((FlexibleStorage) o).size && this.maxEntryValue == ((FlexibleStorage) o).maxEntryValue); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(this.data); + result = 31 * result + this.bitsPerEntry; + result = 31 * result + this.size; + result = 31 * result + (int) this.maxEntryValue; + return result; + } +} diff --git a/src/main/java/org/spacehq/mc/protocol/data/game/NibbleArray3d.java b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java similarity index 72% rename from src/main/java/org/spacehq/mc/protocol/data/game/NibbleArray3d.java rename to src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java index b53d4b74..3d8ebf2b 100644 --- a/src/main/java/org/spacehq/mc/protocol/data/game/NibbleArray3d.java +++ b/src/main/java/org/spacehq/mc/protocol/data/game/chunk/NibbleArray3d.java @@ -1,9 +1,12 @@ -package org.spacehq.mc.protocol.data.game; +package org.spacehq.mc.protocol.data.game.chunk; +import org.spacehq.packetlib.io.NetInput; +import org.spacehq.packetlib.io.NetOutput; + +import java.io.IOException; import java.util.Arrays; public class NibbleArray3d { - private byte[] data; public NibbleArray3d(int size) { @@ -14,6 +17,14 @@ public class NibbleArray3d { this.data = array; } + public NibbleArray3d(NetInput in, int size) throws IOException { + this.data = in.readBytes(size); + } + + public void write(NetOutput out) throws IOException { + out.writeBytes(this.data); + } + public byte[] getData() { return this.data; } @@ -50,19 +61,11 @@ public class NibbleArray3d { @Override public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - NibbleArray3d that = (NibbleArray3d) o; - - if(!Arrays.equals(data, that.data)) return false; - - return true; + return this == o || (o instanceof NibbleArray3d && Arrays.equals(this.data, ((NibbleArray3d) o).data)); } @Override public int hashCode() { - return Arrays.hashCode(data); + return Arrays.hashCode(this.data); } - } diff --git a/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerChunkDataPacket.java b/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerChunkDataPacket.java index 2ad65c51..3e655e92 100644 --- a/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerChunkDataPacket.java +++ b/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerChunkDataPacket.java @@ -1,21 +1,19 @@ package org.spacehq.mc.protocol.packet.ingame.server.world; -import org.spacehq.mc.protocol.data.game.Chunk; +import org.spacehq.mc.protocol.data.game.chunk.Chunk; +import org.spacehq.mc.protocol.data.game.chunk.Column; import org.spacehq.mc.protocol.util.NetUtil; -import org.spacehq.mc.protocol.util.NetworkChunkData; -import org.spacehq.mc.protocol.util.ParsedChunkData; import org.spacehq.packetlib.io.NetInput; import org.spacehq.packetlib.io.NetOutput; +import org.spacehq.packetlib.io.buffer.ByteBufferNetOutput; import org.spacehq.packetlib.packet.Packet; import java.io.IOException; +import java.nio.ByteBuffer; public class ServerChunkDataPacket implements Packet { - private int x; - private int z; - private Chunk chunks[]; - private byte biomeData[]; + private Column column; @SuppressWarnings("unused") private ServerChunkDataPacket() { @@ -28,103 +26,48 @@ public class ServerChunkDataPacket implements Packet { * @param z Z of the chunk column. */ public ServerChunkDataPacket(int x, int z) { - this(x, z, new Chunk[16], new byte[256]); + this(new Column(x, z, new Chunk[16], new byte[256])); } /** * Constructs a ServerChunkDataPacket for updating chunks. * - * @param x X of the chunk column. - * @param z Z of the chunk column. - * @param chunks Array of chunks in the column. Length must be 16 but can contain null values. - * @throws IllegalArgumentException If the chunk array length is not 16 or skylight arrays exist in some but not all chunks. + * @param column Column to send. */ - public ServerChunkDataPacket(int x, int z, Chunk chunks[]) { - this(x, z, chunks, null); + public ServerChunkDataPacket(Column column) { + this.column = column; } - /** - * Constructs a ServerChunkDataPacket for updating a full column of chunks. - * - * @param x X of the chunk column. - * @param z Z of the chunk column. - * @param chunks Array of chunks in the column. Length must be 16 but can contain null values. - * @param biomeData Array of biome data for the column. - * @throws IllegalArgumentException If the chunk array length is not 16 or skylight arrays exist in some but not all chunks. - */ - public ServerChunkDataPacket(int x, int z, Chunk chunks[], byte biomeData[]) { - if(chunks.length != 16) { - throw new IllegalArgumentException("Chunks length must be 16."); - } - - boolean noSkylight = false; - boolean skylight = false; - for(int index = 0; index < chunks.length; index++) { - if(chunks[index] != null) { - if(chunks[index].getSkyLight() == null) { - noSkylight = true; - } else { - skylight = true; - } - } - } - - if(noSkylight && skylight) { - throw new IllegalArgumentException("Either all chunks must have skylight values or none must have them."); - } - - this.x = x; - this.z = z; - this.chunks = chunks; - this.biomeData = biomeData; - } - - public int getX() { - return this.x; - } - - public int getZ() { - return this.z; - } - - public Chunk[] getChunks() { - return this.chunks; - } - - public byte[] getBiomeData() { - return this.biomeData; - } - - public boolean isFullChunk() { - return this.biomeData != null; + public Column getColumn() { + return this.column; } @Override public void read(NetInput in) throws IOException { - this.x = in.readInt(); - this.z = in.readInt(); + int x = in.readInt(); + int z = in.readInt(); boolean fullChunk = in.readBoolean(); - int chunkMask = in.readUnsignedShort(); + int chunkMask = in.readInt(); byte data[] = in.readBytes(in.readVarInt()); - ParsedChunkData chunkData = NetUtil.dataToChunks(new NetworkChunkData(chunkMask, fullChunk, false, data), true); - this.chunks = chunkData.getChunks(); - this.biomeData = chunkData.getBiomes(); + + this.column = NetUtil.readColumn(data, x, z, fullChunk, false, chunkMask); } @Override public void write(NetOutput out) throws IOException { - NetworkChunkData data = NetUtil.chunksToData(new ParsedChunkData(this.chunks, this.biomeData)); - out.writeInt(this.x); - out.writeInt(this.z); - out.writeBoolean(data.isFullChunk()); - out.writeShort(data.getMask()); - out.writeVarInt(data.getData().length); - out.writeBytes(data.getData(), data.getData().length); + ByteBufferNetOutput byteOut = new ByteBufferNetOutput(ByteBuffer.allocate(557312)); + int mask = NetUtil.writeColumn(byteOut, this.column, this.column.hasBiomeData(), this.column.hasSkylight()); + + out.writeInt(this.column.getX()); + out.writeInt(this.column.getZ()); + out.writeBoolean(this.column.hasBiomeData()); + out.writeShort(mask); + out.writeVarInt(byteOut.getByteBuffer().arrayOffset()); + out.writeBytes(byteOut.getByteBuffer().array(), byteOut.getByteBuffer().arrayOffset()); } @Override public boolean isPriority() { return false; } - } diff --git a/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerMultiChunkDataPacket.java b/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerMultiChunkDataPacket.java index 110abf4e..c0e69286 100644 --- a/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerMultiChunkDataPacket.java +++ b/src/main/java/org/spacehq/mc/protocol/packet/ingame/server/world/ServerMultiChunkDataPacket.java @@ -1,133 +1,92 @@ package org.spacehq.mc.protocol.packet.ingame.server.world; -import org.spacehq.mc.protocol.data.game.Chunk; +import org.spacehq.mc.protocol.data.game.chunk.Column; import org.spacehq.mc.protocol.util.NetUtil; -import org.spacehq.mc.protocol.util.NetworkChunkData; -import org.spacehq.mc.protocol.util.ParsedChunkData; import org.spacehq.packetlib.io.NetInput; import org.spacehq.packetlib.io.NetOutput; +import org.spacehq.packetlib.io.buffer.ByteBufferNetOutput; import org.spacehq.packetlib.packet.Packet; import java.io.IOException; +import java.nio.ByteBuffer; public class ServerMultiChunkDataPacket implements Packet { - private int x[]; - private int z[]; - private Chunk chunks[][]; - private byte biomeData[][]; + private Column columns[]; @SuppressWarnings("unused") private ServerMultiChunkDataPacket() { } - public ServerMultiChunkDataPacket(int x[], int z[], Chunk chunks[][], byte biomeData[][]) { - if(biomeData == null) { - throw new IllegalArgumentException("BiomeData cannot be null."); - } - - if(x.length != chunks.length || z.length != chunks.length) { - throw new IllegalArgumentException("X, Z, and Chunk arrays must be equal in length."); - } - + public ServerMultiChunkDataPacket(Column columns[]) { boolean noSkylight = false; boolean skylight = false; - for(int index = 0; index < chunks.length; index++) { - Chunk column[] = chunks[index]; - if(column.length != 16) { - throw new IllegalArgumentException("Chunk columns must contain 16 chunks each."); - } - - for(int y = 0; y < column.length; y++) { - if(column[y] != null) { - if(column[y].getSkyLight() == null) { - noSkylight = true; - } else { - skylight = true; - } - } + for(Column column : columns) { + if(!column.hasSkylight()) { + noSkylight = true; + } else { + skylight = true; } } if(noSkylight && skylight) { - throw new IllegalArgumentException("Either all chunks must have skylight values or none must have them."); + throw new IllegalArgumentException("Either all columns must have skylight values or none must have them."); } - this.x = x; - this.z = z; - this.chunks = chunks; - this.biomeData = biomeData; + this.columns = columns; } - public int getColumns() { - return this.chunks.length; - } - - public int getX(int column) { - return this.x[column]; - } - - public int getZ(int column) { - return this.z[column]; - } - - public Chunk[] getChunks(int column) { - return this.chunks[column]; - } - - public byte[] getBiomeData(int column) { - return this.biomeData[column]; + public Column[] getColumns() { + return this.columns; } @Override public void read(NetInput in) throws IOException { boolean skylight = in.readBoolean(); - int columns = in.readVarInt(); - this.x = new int[columns]; - this.z = new int[columns]; - this.chunks = new Chunk[columns][]; - this.biomeData = new byte[columns][]; - NetworkChunkData[] data = new NetworkChunkData[columns]; - for(int column = 0; column < columns; column++) { - this.x[column] = in.readInt(); - this.z[column] = in.readInt(); - int mask = in.readUnsignedShort(); - int chunks = Integer.bitCount(mask); - int length = (chunks * ((4096 * 2) + 2048)) + (skylight ? chunks * 2048 : 0) + 256; - byte dat[] = new byte[length]; - data[column] = new NetworkChunkData(mask, true, skylight, dat); - } + int x[] = in.readInts(in.readVarInt()); + int z[] = in.readInts(in.readVarInt()); + int masks[] = in.readInts(in.readVarInt()); + byte data[] = in.readBytes(in.readVarInt()); - for(int column = 0; column < columns; column++) { - in.readBytes(data[column].getData()); - ParsedChunkData chunkData = NetUtil.dataToChunks(data[column], false); - this.chunks[column] = chunkData.getChunks(); - this.biomeData[column] = chunkData.getBiomes(); + Column columns[] = new Column[x.length]; + for(int i = 0; i < columns.length; i++) { + columns[i] = NetUtil.readColumn(data, x[i], z[i], true, skylight, masks[i]); } } @Override public void write(NetOutput out) throws IOException { + Column columns[] = new Column[1]; + boolean skylight = false; - NetworkChunkData data[] = new NetworkChunkData[this.chunks.length]; - for(int column = 0; column < this.chunks.length; column++) { - data[column] = NetUtil.chunksToData(new ParsedChunkData(this.chunks[column], this.biomeData[column])); - if(data[column].hasSkyLight()) { + for(Column column : columns) { + if(column.hasSkylight()) { skylight = true; + break; } } + ByteBufferNetOutput byteOut = new ByteBufferNetOutput(ByteBuffer.allocate(columns.length * 557312)); + out.writeBoolean(skylight); - out.writeVarInt(this.chunks.length); - for(int column = 0; column < this.x.length; column++) { - out.writeInt(this.x[column]); - out.writeInt(this.z[column]); - out.writeShort(data[column].getMask()); + + out.writeVarInt(columns.length); + for(Column column : columns) { + out.writeInt(column.getX()); } - for(int column = 0; column < this.x.length; column++) { - out.writeBytes(data[column].getData()); + out.writeVarInt(columns.length); + for(Column column : columns) { + out.writeInt(column.getZ()); } + + out.writeVarInt(columns.length); + for(Column column : columns) { + out.writeInt(NetUtil.writeColumn(byteOut, column, true, skylight)); + } + + out.writeVarInt(byteOut.getByteBuffer().arrayOffset()); + out.writeBytes(byteOut.getByteBuffer().array(), byteOut.getByteBuffer().arrayOffset()); } @Override diff --git a/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java b/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java index 195fa707..5b4e5f82 100644 --- a/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java +++ b/src/main/java/org/spacehq/mc/protocol/util/NetUtil.java @@ -1,12 +1,13 @@ package org.spacehq.mc.protocol.util; -import org.spacehq.mc.protocol.data.game.Chunk; +import org.spacehq.mc.protocol.data.game.chunk.BlockStorage; +import org.spacehq.mc.protocol.data.game.chunk.Chunk; +import org.spacehq.mc.protocol.data.game.chunk.Column; import org.spacehq.mc.protocol.data.game.EntityMetadata; import org.spacehq.mc.protocol.data.game.ItemStack; -import org.spacehq.mc.protocol.data.game.NibbleArray3d; +import org.spacehq.mc.protocol.data.game.chunk.NibbleArray3d; import org.spacehq.mc.protocol.data.game.Position; import org.spacehq.mc.protocol.data.game.Rotation; -import org.spacehq.mc.protocol.data.game.ShortArray3d; import org.spacehq.mc.protocol.data.game.values.MagicValues; import org.spacehq.mc.protocol.data.game.values.entity.MetadataType; import org.spacehq.mc.protocol.data.game.values.world.block.BlockFace; @@ -15,6 +16,7 @@ import org.spacehq.opennbt.NBTIO; import org.spacehq.opennbt.tag.builtin.CompoundTag; import org.spacehq.packetlib.io.NetInput; import org.spacehq.packetlib.io.NetOutput; +import org.spacehq.packetlib.io.buffer.ByteBufferNetInput; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -22,8 +24,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.ShortBuffer; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -55,7 +55,7 @@ public class NetUtil { return i != 0 && (i & i - 1) == 0; } - private static int nextExponentOfTwo(int i) { + public static int nextExponentOfTwo(int i) { int power = isPowerOfTwo(i) ? i : nextPowerOfTwo(i); return EXPONENTS_OF_TWO[(int) (power * 125613361L >> 27) & 31]; } @@ -235,124 +235,61 @@ public class NetUtil { out.writeByte(255); } - public static ParsedChunkData dataToChunks(NetworkChunkData data, boolean checkForSky) { - Chunk chunks[] = new Chunk[16]; - int pos = 0; - int expected = 0; - boolean sky = false; - ShortBuffer buf = ByteBuffer.wrap(data.getData()).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); - // 0 = Calculate expected length and determine if the packet has skylight. - // 1 = Create chunks from mask and get blocks. - // 2 = Get block light. - // 3 = Get sky light. - for(int pass = 0; pass < 4; pass++) { - for(int ind = 0; ind < 16; ind++) { - if((data.getMask() & 1 << ind) != 0) { - if(pass == 0) { - // Block length + Blocklight length - expected += (4096 * 2) + 2048; - } - - if(pass == 1) { - chunks[ind] = new Chunk(sky || data.hasSkyLight()); - ShortArray3d blocks = chunks[ind].getBlocks(); - buf.position(pos / 2); - buf.get(blocks.getData(), 0, blocks.getData().length); - pos += blocks.getData().length * 2; - } - - if(pass == 2) { - NibbleArray3d blocklight = chunks[ind].getBlockLight(); - System.arraycopy(data.getData(), pos, blocklight.getData(), 0, blocklight.getData().length); - pos += blocklight.getData().length; - } - - if(pass == 3 && (sky || data.hasSkyLight())) { - NibbleArray3d skylight = chunks[ind].getSkyLight(); - System.arraycopy(data.getData(), pos, skylight.getData(), 0, skylight.getData().length); - pos += skylight.getData().length; - } + public static Column readColumn(byte data[], int x, int z, boolean fullChunk, boolean hasSkylight, int mask) throws IOException { + NetInput in = new ByteBufferNetInput(ByteBuffer.wrap(data)); + Exception ex = null; + Column column = null; + try { + Chunk[] chunks = new Chunk[16]; + for(int index = 0; index < chunks.length; index++) { + if((mask & (1 << index)) != 0) { + BlockStorage blocks = new BlockStorage(in); + NibbleArray3d blocklight = new NibbleArray3d(in, 2048); + NibbleArray3d skylight = hasSkylight ? new NibbleArray3d(in, 2048) : null; + chunks[index] = new Chunk(blocks, blocklight, skylight); } } - if(pass == 0) { - // If we have more data than blocks and blocklight combined, there must be skylight data as well. - if(data.getData().length >= expected) { - sky = checkForSky; - } + byte biomeData[] = null; + if(fullChunk) { + biomeData = in.readBytes(256); } + + column = new Column(x, z, chunks, biomeData); + } catch(Exception e) { + ex = e; } - byte biomeData[] = null; - if(data.isFullChunk()) { - biomeData = new byte[256]; - System.arraycopy(data.getData(), pos, biomeData, 0, biomeData.length); - pos += biomeData.length; + // Unfortunately, this is needed to detect whether the chunks contain skylight or not. + if((in.available() > 0 || ex != null) && !hasSkylight) { + return readColumn(data, x, z, fullChunk, true, mask); + } else if(ex != null) { + throw new IOException("Failed to read chunk data.", ex); } - return new ParsedChunkData(chunks, biomeData); + return column; } - public static NetworkChunkData chunksToData(ParsedChunkData chunks) { - int chunkMask = 0; - boolean fullChunk = chunks.getBiomes() != null; - boolean sky = false; - int length = fullChunk ? chunks.getBiomes().length : 0; - byte[] data = null; - int pos = 0; - ShortBuffer buf = null; - // 0 = Determine length and masks. - // 1 = Add blocks. - // 2 = Add block light. - // 3 = Add sky light. - for(int pass = 0; pass < 4; pass++) { - for(int ind = 0; ind < chunks.getChunks().length; ++ind) { - Chunk chunk = chunks.getChunks()[ind]; - if(chunk != null && (!fullChunk || !chunk.isEmpty())) { - if(pass == 0) { - chunkMask |= 1 << ind; - length += chunk.getBlocks().getData().length * 2; - length += chunk.getBlockLight().getData().length; - if(chunk.getSkyLight() != null) { - length += chunk.getSkyLight().getData().length; - } - } - - if(pass == 1) { - short blocks[] = chunk.getBlocks().getData(); - buf.position(pos / 2); - buf.put(blocks, 0, blocks.length); - pos += blocks.length * 2; - } - - if(pass == 2) { - byte blocklight[] = chunk.getBlockLight().getData(); - System.arraycopy(blocklight, 0, data, pos, blocklight.length); - pos += blocklight.length; - } - - if(pass == 3 && chunk.getSkyLight() != null) { - byte skylight[] = chunk.getSkyLight().getData(); - System.arraycopy(skylight, 0, data, pos, skylight.length); - pos += skylight.length; - sky = true; - } + public static int writeColumn(NetOutput out, Column column, boolean fullChunk, boolean hasSkylight) throws IOException { + int mask = 0; + Chunk chunks[] = column.getChunks(); + for(int index = 0; index < chunks.length; index++) { + Chunk chunk = chunks[index]; + if(chunk != null && (!fullChunk || !chunk.isEmpty())) { + mask |= 1 << index; + chunk.getBlocks().write(out); + chunk.getBlockLight().write(out); + if(hasSkylight) { + chunk.getBlockLight().write(out); } } - - if(pass == 0) { - data = new byte[length]; - buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); - } } - // Add biomes. if(fullChunk) { - System.arraycopy(chunks.getBiomes(), 0, data, pos, chunks.getBiomes().length); - pos += chunks.getBiomes().length; + out.writeBytes(column.getBiomeData()); } - return new NetworkChunkData(chunkMask, fullChunk, sky, data); + return mask; } private static class NetInputStream extends InputStream { diff --git a/src/main/java/org/spacehq/mc/protocol/util/NetworkChunkData.java b/src/main/java/org/spacehq/mc/protocol/util/NetworkChunkData.java deleted file mode 100644 index c7c8c6e9..00000000 --- a/src/main/java/org/spacehq/mc/protocol/util/NetworkChunkData.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.spacehq.mc.protocol.util; - -public class NetworkChunkData { - - private int mask; - private boolean fullChunk; - private boolean sky; - private byte data[]; - - public NetworkChunkData(int mask, boolean fullChunk, boolean sky, byte data[]) { - this.mask = mask; - this.fullChunk = fullChunk; - this.sky = sky; - this.data = data; - } - - public int getMask() { - return this.mask; - } - - public boolean isFullChunk() { - return this.fullChunk; - } - - public boolean hasSkyLight() { - return this.sky; - } - - public byte[] getData() { - return this.data; - } - - @Override - public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - NetworkChunkData that = (NetworkChunkData) o; - - if(fullChunk != that.fullChunk) return false; - if(mask != that.mask) return false; - if(sky != that.sky) return false; - - return true; - } - - @Override - public int hashCode() { - int result = mask; - result = 31 * result + (fullChunk ? 1 : 0); - result = 31 * result + (sky ? 1 : 0); - return result; - } - -} diff --git a/src/main/java/org/spacehq/mc/protocol/util/ParsedChunkData.java b/src/main/java/org/spacehq/mc/protocol/util/ParsedChunkData.java deleted file mode 100644 index c328b96b..00000000 --- a/src/main/java/org/spacehq/mc/protocol/util/ParsedChunkData.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.spacehq.mc.protocol.util; - -import org.spacehq.mc.protocol.data.game.Chunk; - -import java.util.Arrays; - -public class ParsedChunkData { - - private Chunk chunks[]; - private byte biomes[]; - - public ParsedChunkData(Chunk chunks[], byte biomes[]) { - this.chunks = chunks; - this.biomes = biomes; - } - - public Chunk[] getChunks() { - return this.chunks; - } - - public byte[] getBiomes() { - return this.biomes; - } - - @Override - public boolean equals(Object o) { - if(this == o) return true; - if(o == null || getClass() != o.getClass()) return false; - - ParsedChunkData that = (ParsedChunkData) o; - - if(!Arrays.equals(biomes, that.biomes)) return false; - if(!Arrays.equals(chunks, that.chunks)) return false; - - return true; - } - - @Override - public int hashCode() { - int result = Arrays.hashCode(chunks); - result = 31 * result + (biomes != null ? Arrays.hashCode(biomes) : 0); - return result; - } - -}