Revise chunk storage code based on latest logic.

This commit is contained in:
Steveice10 2020-10-13 16:18:56 -07:00
parent ddd5b3a034
commit 2ac116a075
8 changed files with 294 additions and 96 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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<Integer> 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;
}
}

View file

@ -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<Integer> 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<Integer> 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<Integer> 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;
}
}

View file

@ -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;
}
}

View file

@ -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<Integer> 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;
}
}
}

View file

@ -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<Integer, Integer> idToState = new HashMap<>();
private final Map<Integer, Integer> 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;
}
}
}

View file

@ -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);
}