diff --git a/example/com/github/steveice10/mc/protocol/test/MinecraftProtocolTest.java b/example/com/github/steveice10/mc/protocol/test/MinecraftProtocolTest.java index 39ba6ccc..8877317f 100644 --- a/example/com/github/steveice10/mc/protocol/test/MinecraftProtocolTest.java +++ b/example/com/github/steveice10/mc/protocol/test/MinecraftProtocolTest.java @@ -198,7 +198,6 @@ public class MinecraftProtocolTest { private static void login() { MinecraftProtocol protocol = null; if(VERIFY_USERS) { - try { AuthenticationService authService = new AuthenticationService(); authService.setUsername(USERNAME); diff --git a/src/main/java/com/github/steveice10/mc/protocol/MinecraftProtocol.java b/src/main/java/com/github/steveice10/mc/protocol/MinecraftProtocol.java index 48c29a00..8e228a49 100644 --- a/src/main/java/com/github/steveice10/mc/protocol/MinecraftProtocol.java +++ b/src/main/java/com/github/steveice10/mc/protocol/MinecraftProtocol.java @@ -170,7 +170,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import java.net.Proxy; import java.security.GeneralSecurityException; import java.security.Key; import java.util.UUID; diff --git a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/FlexibleStorage.java b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/BitStorage.java similarity index 56% rename from src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/FlexibleStorage.java rename to src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/BitStorage.java index a2f41e1f..f591a61b 100644 --- a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/FlexibleStorage.java +++ b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/BitStorage.java @@ -1,24 +1,11 @@ package com.github.steveice10.mc.protocol.data.game.chunk; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NonNull; -import java.util.Arrays; -import java.util.function.IntFunction; - -@Data -public class FlexibleStorage { - private final @NonNull long[] data; - private final int bitsPerEntry; - private final int size; - private final long maxEntryValue; - - private final char valuesPerLong; - private final int magicIndex; - private final long divideMultiply; - private final long divideAdd; - private final long divideShift; - +@EqualsAndHashCode +public class BitStorage { private static final int[] MAGIC_VALUES = { -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, @@ -39,25 +26,48 @@ public class FlexibleStorage { 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5 }; + 0, 5 + }; - public FlexibleStorage(int bitsPerEntry) { - this(bitsPerEntry, new long[(4096 + (64 / bitsPerEntry) - 1) / (64 / bitsPerEntry)]); + @Getter + private final @NonNull long[] data; + @Getter + private final int bitsPerEntry; + @Getter + private final int size; + + private final long maxValue; + private final int valuesPerLong; + private final long divideMultiply; + private final long divideAdd; + private final int divideShift; + + public BitStorage(int bitsPerEntry, int size) { + this(bitsPerEntry, size, null); } - public FlexibleStorage(int bitsPerEntry, @NonNull long[] data) { - this.bitsPerEntry = bitsPerEntry; - this.data = Arrays.copyOf(data, data.length); - this.size = data.length * 64 / bitsPerEntry; - this.maxEntryValue = (1L << bitsPerEntry) - 1; - - this.valuesPerLong = (char) (64 / bitsPerEntry); - int expectedLength = (4096 + valuesPerLong - 1) / valuesPerLong; - if(data.length != expectedLength) { - throw new IllegalArgumentException("Expected " + expectedLength + " longs but got " + data.length + " longs"); + public BitStorage(int bitsPerEntry, int size, long[] data) { + if(bitsPerEntry < 1 || bitsPerEntry > 32) { + throw new IllegalArgumentException("bitsPerEntry must be between 1 and 32, inclusive."); } - this.magicIndex = 3 * (valuesPerLong - 1); + this.bitsPerEntry = bitsPerEntry; + this.size = size; + + this.maxValue = (1L << bitsPerEntry) - 1L; + this.valuesPerLong = (char) (64 / bitsPerEntry); + int expectedLength = (size + this.valuesPerLong - 1) / this.valuesPerLong; + if(data != null) { + if(data.length != expectedLength) { + throw new IllegalArgumentException("Expected " + expectedLength + " longs but got " + data.length + " longs"); + } + + this.data = data; + } else { + this.data = new long[expectedLength]; + } + + int magicIndex = 3 * (this.valuesPerLong - 1); this.divideMultiply = Integer.toUnsignedLong(MAGIC_VALUES[magicIndex]); this.divideAdd = Integer.toUnsignedLong(MAGIC_VALUES[magicIndex + 1]); this.divideShift = MAGIC_VALUES[magicIndex + 2]; @@ -68,9 +78,9 @@ public class FlexibleStorage { throw new IndexOutOfBoundsException(); } - int cellIndex = (int) (index * divideMultiply + divideAdd >> 32L >> divideShift); - int bitIndex = (index - cellIndex * valuesPerLong) * bitsPerEntry; - return (int) (data[cellIndex] >> bitIndex & maxEntryValue); + int cellIndex = cellIndex(index); + int bitIndex = bitIndex(index, cellIndex); + return (int) (this.data[cellIndex] >> bitIndex & this.maxValue); } public void set(int index, int value) { @@ -78,20 +88,37 @@ public class FlexibleStorage { throw new IndexOutOfBoundsException(); } - if(value < 0 || value > this.maxEntryValue) { + if(value < 0 || value > this.maxValue) { throw new IllegalArgumentException("Value cannot be outside of accepted range."); } - int cellIndex = (int) (index * divideMultiply + divideAdd >> 32L >> divideShift); - int bitIndex = (index - cellIndex * valuesPerLong) * bitsPerEntry; - this.data[cellIndex] = this.data[cellIndex] & ~(this.maxEntryValue << bitIndex) | ((long) value & this.maxEntryValue) << bitIndex; + int cellIndex = cellIndex(index); + int bitIndex = bitIndex(index, cellIndex); + this.data[cellIndex] = this.data[cellIndex] & ~(this.maxValue << bitIndex) | ((long) value & this.maxValue) << bitIndex; } - public FlexibleStorage transferData(int newBitsPerEntry, IntFunction valueGetter) { - FlexibleStorage newStorage = new FlexibleStorage(newBitsPerEntry); - for(int i = 0; i < 4096; i++) { - newStorage.set(i, valueGetter.apply(i)); + public int[] toIntArray() { + int[] result = new int[this.size]; + int index = 0; + for(long cell : this.data) { + for(int bitIndex = 0; bitIndex < this.valuesPerLong; bitIndex++) { + result[index++] = (int) (cell & this.maxValue); + cell >>= this.bitsPerEntry; + + if (index >= this.size) { + return result; + } + } } - return newStorage; + + return result; + } + + private int cellIndex(int index) { + return (int) (index * this.divideMultiply + this.divideAdd >> 32 >> this.divideShift); + } + + private int bitIndex(int index, int cellIndex) { + return (index - cellIndex * this.valuesPerLong) * this.bitsPerEntry; } } diff --git a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/Chunk.java b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/Chunk.java index 2eef637f..cdf9add2 100644 --- a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/Chunk.java +++ b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/Chunk.java @@ -1,110 +1,126 @@ package com.github.steveice10.mc.protocol.data.game.chunk; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.GlobalPalette; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.ListPalette; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.MapPalette; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette; import com.github.steveice10.packetlib.io.NetInput; import com.github.steveice10.packetlib.io.NetOutput; import lombok.*; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; @Data @Setter(AccessLevel.NONE) @AllArgsConstructor public class Chunk { + private static final int CHUNK_SIZE = 4096; + private static final int MIN_PALETTE_BITS_PER_ENTRY = 4; + private static final int MAX_PALETTE_BITS_PER_ENTRY = 8; + private static final int GLOBAL_PALETTE_BITS_PER_ENTRY = 14; + private static final int AIR = 0; private int blockCount; - private int bitsPerEntry; - - private @NonNull List palette; - private @NonNull FlexibleStorage storage; + private @NonNull Palette palette; + private @NonNull BitStorage storage; public Chunk() { - this(0, 4, new ArrayList<>(Collections.singletonList(AIR)), new FlexibleStorage(4)); + this(0, new ListPalette(MIN_PALETTE_BITS_PER_ENTRY), new BitStorage(MIN_PALETTE_BITS_PER_ENTRY, CHUNK_SIZE)); } public static Chunk read(NetInput in) throws IOException { int blockCount = in.readShort(); int bitsPerEntry = in.readUnsignedByte(); - List palette = new ArrayList<>(); - if(bitsPerEntry <= 8) { + Palette palette = createPalette(bitsPerEntry); + if(!(palette instanceof GlobalPalette)) { int paletteLength = in.readVarInt(); for(int i = 0; i < paletteLength; i++) { - palette.add(in.readVarInt()); + palette.stateToId(in.readVarInt()); } } - FlexibleStorage storage = new FlexibleStorage(bitsPerEntry, in.readLongs(in.readVarInt())); - return new Chunk(blockCount, bitsPerEntry, palette, storage); + BitStorage storage = new BitStorage(bitsPerEntry, CHUNK_SIZE, in.readLongs(in.readVarInt())); + return new Chunk(blockCount, palette, storage); } public static void write(NetOutput out, Chunk chunk) throws IOException { - out.writeShort(chunk.getBlockCount()); - out.writeByte(chunk.getBitsPerEntry()); + out.writeShort(chunk.blockCount); + out.writeByte(chunk.storage.getBitsPerEntry()); - if(chunk.getBitsPerEntry() <= 8) { - out.writeVarInt(chunk.getPalette().size()); - for (int state : chunk.getPalette()) { - out.writeVarInt(state); + if(!(chunk.palette instanceof GlobalPalette)) { + int paletteLength = chunk.palette.size(); + out.writeVarInt(paletteLength); + for(int i = 0; i < paletteLength; i++) { + out.writeVarInt(chunk.palette.idToState(i)); } } - long[] data = chunk.getStorage().getData(); + long[] data = chunk.storage.getData(); out.writeVarInt(data.length); out.writeLongs(data); } - private static int index(int x, int y, int z) { - return y << 8 | z << 4 | x; - } - public int get(int x, int y, int z) { int id = this.storage.get(index(x, y, z)); - return this.bitsPerEntry <= 8 ? (id >= 0 && id < this.palette.size() ? this.palette.get(id) : AIR) : id; + return this.palette.idToState(id); } public void set(int x, int y, int z, @NonNull int state) { - int id = this.bitsPerEntry <= 8 ? this.palette.indexOf(state) : state; + int id = this.palette.stateToId(state); if(id == -1) { - this.palette.add(state); - if(this.palette.size() > 1 << this.bitsPerEntry) { - this.bitsPerEntry++; - - final List oldStates = this.bitsPerEntry > 8 ? new ArrayList<>(this.palette) : this.palette; - if(this.bitsPerEntry > 8) { - this.palette.clear(); - this.bitsPerEntry = 14; - } - - FlexibleStorage oldStorage = this.storage; - this.storage = oldStorage.transferData(this.bitsPerEntry, (index) -> - this.bitsPerEntry <= 8 ? oldStorage.get(index) : oldStates.get(index)); - } - - id = this.bitsPerEntry <= 8 ? this.palette.indexOf(state) : state; + this.resizePalette(); + id = this.palette.stateToId(state); } - int ind = index(x, y, z); - int curr = this.storage.get(ind); + int index = index(x, y, z); + int curr = this.storage.get(index); if(state != AIR && curr == AIR) { this.blockCount++; } else if(state == AIR && curr != AIR) { this.blockCount--; } - this.storage.set(ind, id); + this.storage.set(index, id); } public boolean isEmpty() { - for(int index = 0; index < this.storage.getSize(); index++) { - if(this.storage.get(index) != 0) { - return false; - } - } + return this.blockCount == 0; + } - return true; + private int sanitizeBitsPerEntry(int bitsPerEntry) { + if(bitsPerEntry <= MAX_PALETTE_BITS_PER_ENTRY) { + return Math.max(MIN_PALETTE_BITS_PER_ENTRY, bitsPerEntry); + } else { + return GLOBAL_PALETTE_BITS_PER_ENTRY; + } + } + + private void resizePalette() { + Palette oldPalette = this.palette; + int[] oldData = this.storage.toIntArray(); + + int bitsPerEntry = sanitizeBitsPerEntry(this.storage.getBitsPerEntry() + 1); + this.palette = createPalette(bitsPerEntry); + this.storage = new BitStorage(bitsPerEntry, CHUNK_SIZE); + + for(int i = 0; i < oldData.length; i++) { + this.storage.set(i, this.palette.stateToId(oldPalette.idToState(oldData[i]))); + } + } + + private static Palette createPalette(int bitsPerEntry) { + if(bitsPerEntry <= MIN_PALETTE_BITS_PER_ENTRY) { + return new ListPalette(bitsPerEntry); + } else if(bitsPerEntry <= MAX_PALETTE_BITS_PER_ENTRY) { + return new MapPalette(bitsPerEntry); + } else { + return new GlobalPalette(); + } + } + + private static int index(int x, int y, int z) { + return y << 8 | z << 4 | x; } } diff --git a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/GlobalPalette.java b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/GlobalPalette.java new file mode 100644 index 00000000..b17bb51e --- /dev/null +++ b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/GlobalPalette.java @@ -0,0 +1,24 @@ +package com.github.steveice10.mc.protocol.data.game.chunk.palette; + +import lombok.EqualsAndHashCode; + +/** + * A global palette that maps 1:1. + */ +@EqualsAndHashCode +public class GlobalPalette implements Palette { + @Override + public int size() { + return Integer.MAX_VALUE; + } + + @Override + public int stateToId(int state) { + return state; + } + + @Override + public int idToState(int id) { + return id; + } +} diff --git a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/ListPalette.java b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/ListPalette.java new file mode 100644 index 00000000..3c1c07eb --- /dev/null +++ b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/ListPalette.java @@ -0,0 +1,47 @@ +package com.github.steveice10.mc.protocol.data.game.chunk.palette; + +import lombok.EqualsAndHashCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * A palette backed by a List. + */ +@EqualsAndHashCode +public class ListPalette implements Palette { + private final int maxId; + + private final List data = new ArrayList<>(); + + public ListPalette(int bitsPerEntry) { + this.maxId = (1 << bitsPerEntry) - 1; + + this.data.add(0); + } + + @Override + public int size() { + return this.data.size(); + } + + @Override + public int stateToId(int state) { + int id = this.data.indexOf(state); + if(id == -1 && this.size() < this.maxId + 1) { + this.data.add(id); + id = this.data.size() - 1; + } + + return id; + } + + @Override + public int idToState(int id) { + if(id >= 0 && id < this.data.size()) { + return this.data.get(id); + } else { + return 0; + } + } +} diff --git a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/MapPalette.java b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/MapPalette.java new file mode 100644 index 00000000..2f468be8 --- /dev/null +++ b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/MapPalette.java @@ -0,0 +1,56 @@ +package com.github.steveice10.mc.protocol.data.game.chunk.palette; + +import lombok.EqualsAndHashCode; + +import java.util.HashMap; +import java.util.Map; + +/** + * A palette backed by a map. + */ +@EqualsAndHashCode +public class MapPalette implements Palette { + private final int maxId; + + private final Map idToState = new HashMap<>(); + private final Map stateToId = new HashMap<>(); + private int nextId = 1; + + public MapPalette(int bitsPerEntry) { + this.maxId = (1 << bitsPerEntry) - 1; + + this.idToState.put(0, 0); + this.stateToId.put(0, 0); + } + + @Override + public int size() { + return this.idToState.size(); + } + + @Override + public int stateToId(int state) { + Integer id = this.stateToId.get(state); + if(id == null && this.size() < this.maxId + 1) { + id = this.nextId++; + this.idToState.put(id, state); + this.stateToId.put(state, id); + } + + if(id != null) { + return id; + } else { + return -1; + } + } + + @Override + public int idToState(int id) { + Integer state = this.idToState.get(id); + if(state != null) { + return state; + } else { + return 0; + } + } +} diff --git a/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/Palette.java b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/Palette.java new file mode 100644 index 00000000..2ae59b8a --- /dev/null +++ b/src/main/java/com/github/steveice10/mc/protocol/data/game/chunk/palette/Palette.java @@ -0,0 +1,30 @@ +package com.github.steveice10.mc.protocol.data.game.chunk.palette; + +/** + * A palette for mapping block states to storage IDs. + */ +public interface Palette { + /** + * Gets the number of block states known by this palette. + * @return The palette's size. + */ + int size(); + + /** + * Converts a block state to a storage ID. If the state has not been mapped, + * the palette will attempt to map it, returning -1 if it cannot. + * + * @param state Block state to convert. + * @return The resulting storage ID. + */ + public int stateToId(int state); + + /** + * Converts a storage ID to a block state. If the storage ID has no mapping, + * it will return a block state of 0. + * + * @param id Storage ID to convert. + * @return The resulting block state. + */ + public int idToState(int id); +}