Prepare for negative Y values

This commit is contained in:
t00thpick1 2021-02-12 17:18:52 -05:00
parent 085c8dbf68
commit f9144e624a
5 changed files with 138 additions and 31 deletions

View File

@ -148,10 +148,13 @@ public class BlockListener implements Listener {
// Get opposite direction so we get correct block // Get opposite direction so we get correct block
BlockFace direction = event.getDirection(); BlockFace direction = event.getDirection();
Block movedBlock = event.getBlock().getRelative(direction); Block movedBlock = event.getBlock().getRelative(direction);
if (movedBlock.getY() >= Misc.getWorldMinCompat(movedBlock.getWorld())) // Very weird that the event is giving us these, they shouldn't exist
mcMMO.getPlaceStore().setTrue(movedBlock); mcMMO.getPlaceStore().setTrue(movedBlock);
for (Block block : event.getBlocks()) { for (Block block : event.getBlocks()) {
movedBlock = block.getRelative(direction); movedBlock = block.getRelative(direction);
if (movedBlock.getY() < Misc.getWorldMinCompat(movedBlock.getWorld())) // Very weird that the event is giving us these, they shouldn't exist
continue;
mcMMO.getPlaceStore().setTrue(movedBlock); mcMMO.getPlaceStore().setTrue(movedBlock);
} }
} }

View File

@ -9,6 +9,7 @@ import com.gmail.nossr50.util.player.UserManager;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.BlockState; import org.bukkit.block.BlockState;
import org.bukkit.entity.*; import org.bukkit.entity.*;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
@ -259,6 +260,12 @@ public final class Misc {
} }
} }
public static int getWorldMinCompat(World world)
{
// TODO this method should access the world min variable in a version safe manner so that we don't restrict usage to new versions of spigot only
return 0;
}
public static void printProgress(int convertedUsers, int progressInterval, long startMillis) { public static void printProgress(int convertedUsers, int progressInterval, long startMillis) {
if ((convertedUsers % progressInterval) == 0) { if ((convertedUsers % progressInterval) == 0) {
mcMMO.p.getLogger().info(String.format("Conversion progress: %d users at %.2f users/second", convertedUsers, convertedUsers / (double) ((System.currentTimeMillis() - startMillis) / TIME_CONVERSION_FACTOR))); mcMMO.p.getLogger().info(String.format("Conversion progress: %d users at %.2f users/second", convertedUsers, convertedUsers / (double) ((System.currentTimeMillis() - startMillis) / TIME_CONVERSION_FACTOR)));

View File

@ -1,5 +1,6 @@
package com.gmail.nossr50.util.blockmeta; package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.util.Misc;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.World; import org.bukkit.World;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -10,12 +11,13 @@ import java.util.BitSet;
import java.util.UUID; import java.util.UUID;
public class BitSetChunkStore implements ChunkStore { public class BitSetChunkStore implements ChunkStore {
private static final int CURRENT_VERSION = 8; private static final int CURRENT_VERSION = 9;
private static final int MAGIC_NUMBER = 0xEA5EDEBB; private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private final int cx; private final int cx;
private final int cz; private final int cz;
private final int worldHeight; private final int worldMin;
private final int worldMax;
private final @NotNull UUID worldUid; private final @NotNull UUID worldUid;
// Bitset store conforms to a "bottom-up" bit ordering consisting of a stack of {worldHeight} Y planes, each Y plane consists of 16 Z rows of 16 X bits. // Bitset store conforms to a "bottom-up" bit ordering consisting of a stack of {worldHeight} Y planes, each Y plane consists of 16 Z rows of 16 X bits.
private final @NotNull BitSet store; private final @NotNull BitSet store;
@ -23,15 +25,16 @@ public class BitSetChunkStore implements ChunkStore {
private transient boolean dirty = false; private transient boolean dirty = false;
public BitSetChunkStore(@NotNull World world, int cx, int cz) { public BitSetChunkStore(@NotNull World world, int cx, int cz) {
this(world.getUID(), world.getMaxHeight(), cx, cz); this(world.getUID(), Misc.getWorldMinCompat(world), world.getMaxHeight(), cx, cz);
} }
private BitSetChunkStore(@NotNull UUID worldUid, int worldHeight, int cx, int cz) { private BitSetChunkStore(@NotNull UUID worldUid, int worldMin, int worldMax, int cx, int cz) {
this.cx = cx; this.cx = cx;
this.cz = cz; this.cz = cz;
this.worldUid = worldUid; this.worldUid = worldUid;
this.worldHeight = worldHeight; this.worldMin = worldMin;
this.store = new BitSet(16 * 16 * worldHeight); this.worldMax = worldMax;
this.store = new BitSet(16 * 16 * (worldMax - worldMin));
} }
@Override @Override
@ -54,6 +57,16 @@ public class BitSetChunkStore implements ChunkStore {
return cz; return cz;
} }
@Override
public int getChunkMin() {
return worldMin;
}
@Override
public int getChunkMax() {
return worldMax;
}
@Override @Override
public @NotNull UUID getWorldId() { public @NotNull UUID getWorldId() {
return worldUid; return worldUid;
@ -86,22 +99,34 @@ public class BitSetChunkStore implements ChunkStore {
} }
private int coordToIndex(int x, int y, int z) { private int coordToIndex(int x, int y, int z) {
return coordToIndex(x, y, z, worldHeight); return coordToIndex(x, y, z, worldMin, worldMax);
} }
private static int coordToIndex(int x, int y, int z, int worldHeight) { private static int coordToIndex(int x, int y, int z, int worldMin, int worldMax) {
if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16) if (x < 0 || x >= 16 || y < worldMin || y >= worldMax || z < 0 || z >= 16)
throw new IndexOutOfBoundsException(String.format("x: %d y: %d z: %d World Height: %d", x, y, z, worldHeight)); throw new IndexOutOfBoundsException(String.format("x: %d y: %d z: %d World Min: %d World Max: %d", x, y, z, worldMin, worldMax));
return (z * 16 + x) + (256 * y); int yOffset = -worldMin; // Ensures y multiplier remains positive
return (z * 16 + x) + (256 * (y + yOffset));
} }
private static int getWorldHeight(@NotNull UUID worldUid, int storedWorldHeight) private static int getWorldMin(@NotNull UUID worldUid, int storedWorldMin)
{ {
World world = Bukkit.getWorld(worldUid); World world = Bukkit.getWorld(worldUid);
// Not sure how this case could come up, but might as well handle it gracefully. Loading a chunkstore for an unloaded world? // Not sure how this case could come up, but might as well handle it gracefully. Loading a chunkstore for an unloaded world?
if (world == null) if (world == null)
return storedWorldHeight; return storedWorldMin;
return Misc.getWorldMinCompat(world);
}
private static int getWorldMax(@NotNull UUID worldUid, int storedWorldMax)
{
World world = Bukkit.getWorld(worldUid);
// Not sure how this case could come up, but might as well handle it gracefully. Loading a chunkstore for an unloaded world?
if (world == null)
return storedWorldMax;
return world.getMaxHeight(); return world.getMaxHeight();
} }
@ -114,7 +139,8 @@ public class BitSetChunkStore implements ChunkStore {
out.writeLong(worldUid.getMostSignificantBits()); out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx); out.writeInt(cx);
out.writeInt(cz); out.writeInt(cz);
out.writeInt(worldHeight); out.writeInt(worldMin);
out.writeInt(worldMax);
// Store the byte array directly so we don't have the object type info overhead // Store the byte array directly so we don't have the object type info overhead
byte[] storeData = store.toByteArray(); byte[] storeData = store.toByteArray();
@ -129,7 +155,7 @@ public class BitSetChunkStore implements ChunkStore {
// Can be used to determine the format of the file // Can be used to determine the format of the file
int fileVersionNumber = in.readInt(); int fileVersionNumber = in.readInt();
if (magic != MAGIC_NUMBER || fileVersionNumber != CURRENT_VERSION) if (magic != MAGIC_NUMBER || fileVersionNumber < 8)
throw new IOException(); throw new IOException();
long lsb = in.readLong(); long lsb = in.readLong();
@ -138,21 +164,38 @@ public class BitSetChunkStore implements ChunkStore {
int cx = in.readInt(); int cx = in.readInt();
int cz = in.readInt(); int cz = in.readInt();
int worldHeight = in.readInt(); int worldMin = 0;
if (fileVersionNumber >= 9)
worldMin = in.readInt();
int worldMax = in.readInt();
byte[] temp = new byte[in.readInt()]; byte[] temp = new byte[in.readInt()];
in.readFully(temp); in.readFully(temp);
BitSet stored = BitSet.valueOf(temp); BitSet stored = BitSet.valueOf(temp);
int currentWorldHeight = getWorldHeight(worldUid, worldHeight); int currentWorldMin = getWorldMin(worldUid, worldMin);
int currentWorldMax = getWorldMax(worldUid, worldMax);
boolean worldHeightShrunk = currentWorldHeight < worldHeight; // The order in which the world height update code occurs here is important, the world max truncate math only holds up if done before adjusting for min changes
// Lop off extra data if world height has shrunk // Lop off extra data if world max has shrunk
if (worldHeightShrunk) if (currentWorldMax < worldMax)
stored.clear(coordToIndex(16, currentWorldHeight, 16, worldHeight), stored.length()); stored.clear(coordToIndex(16, currentWorldMax, 16, worldMin, worldMax), stored.length());
// Left shift store if world min has shrunk
if (currentWorldMin > worldMin)
stored = stored.get(currentWorldMin, stored.length()); // Because BitSet's aren't fixed size, a "substring" operation is equivalent to a left shift
// Right shift store if world min has expanded
if (currentWorldMin < worldMin)
{
int offset = (worldMin - currentWorldMin) * 16 * 16; // We are adding this many bits to the front
// This isn't the most efficient way to do this, however, its a rare case to occur, and in the grand scheme of things, the small performance we could gain would cost us significant reduced readability of the code
BitSet shifted = new BitSet();
for (int i = 0; i < stored.length(); i++)
shifted.set(i + offset, stored.get(i));
stored = shifted;
}
BitSetChunkStore chunkStore = new BitSetChunkStore(worldUid, currentWorldHeight, cx, cz); BitSetChunkStore chunkStore = new BitSetChunkStore(worldUid, currentWorldMin, currentWorldMax, cx, cz);
chunkStore.store.or(stored); chunkStore.store.or(stored);
chunkStore.dirty = worldHeightShrunk; // In the expanded case there is no reason to re-write it unless the data changes chunkStore.dirty = currentWorldMin != worldMin || currentWorldMax != worldMax;
return chunkStore; return chunkStore;
} }
@ -203,7 +246,7 @@ public class BitSetChunkStore implements ChunkStore {
private int cx; private int cx;
private int cz; private int cz;
private int worldHeight; private int worldMax;
private UUID worldUid; private UUID worldUid;
private boolean[][][] store; private boolean[][][] store;
@ -226,19 +269,20 @@ public class BitSetChunkStore implements ChunkStore {
cz = in.readInt(); cz = in.readInt();
store = (boolean[][][]) in.readObject(); store = (boolean[][][]) in.readObject();
worldHeight = store[0][0].length; worldMax = store[0][0].length;
} }
public @NotNull BitSetChunkStore convert() public @NotNull BitSetChunkStore convert()
{ {
int currentWorldHeight = getWorldHeight(worldUid, worldHeight); int currentWorldMin = getWorldMin(worldUid, 0);
int currentWorldMax = getWorldMax(worldUid, worldMax);
BitSetChunkStore converted = new BitSetChunkStore(worldUid, currentWorldHeight, cx, cz); BitSetChunkStore converted = new BitSetChunkStore(worldUid, currentWorldMin, currentWorldMax, cx, cz);
// Read old data into new chunkstore // Read old data into new chunkstore
for (int x = 0; x < 16; x++) { for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
for (int y = 0; y < worldHeight && y < currentWorldHeight; y++) { for (int y = 0; y < worldMax && y < currentWorldMax; y++) {
converted.store.set(converted.coordToIndex(x, y, z), store[x][z][y]); converted.store.set(converted.coordToIndex(x, y, z), store[x][z][y]);
} }
} }

View File

@ -36,6 +36,9 @@ public interface ChunkStore {
*/ */
int getChunkZ(); int getChunkZ();
int getChunkMin();
int getChunkMax();
@NotNull UUID getWorldId(); @NotNull UUID getWorldId();
/** /**

View File

@ -1,6 +1,7 @@
package com.gmail.nossr50.util.blockmeta; package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.TestUtil; import com.gmail.nossr50.TestUtil;
import com.gmail.nossr50.util.Misc;
import com.google.common.io.Files; import com.google.common.io.Files;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.World; import org.bukkit.World;
@ -22,7 +23,7 @@ import static org.mockito.Mockito.mock;
* Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies that the serialization isn't completely broken. * Could be a lot better. But some tests are better than none! Tests the major things, still kinda unit-testy. Verifies that the serialization isn't completely broken.
*/ */
@RunWith(PowerMockRunner.class) @RunWith(PowerMockRunner.class)
@PrepareForTest(Bukkit.class) @PrepareForTest({ Bukkit.class, Misc.class })
public class ChunkStoreTest { public class ChunkStoreTest {
private static File tempDir; private static File tempDir;
@BeforeClass @BeforeClass
@ -76,6 +77,34 @@ public class ChunkStoreTest {
assertEqual(original, deserialized); assertEqual(original, deserialized);
} }
@Test
public void testNegativeWorldMin() throws IOException {
PowerMockito.mockStatic(Misc.class);
Mockito.when(Misc.getWorldMinCompat(mockWorld)).thenReturn(-64);
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, -32, 12);
original.setTrue(14, -64, 12);
original.setTrue(13, -63, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
public void testNegativeWorldMinUpgrade() throws IOException {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 1, 12);
original.setTrue(14, 2, 12);
original.setTrue(13, 3, 12);
byte[] serializedBytes = serializeChunkstore(original);
PowerMockito.mockStatic(Misc.class);
Mockito.when(Misc.getWorldMinCompat(mockWorld)).thenReturn(-64);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqualIgnoreMinMax(original, deserialized);
}
@Test @Test
public void testChunkCoords() throws IOException { public void testChunkCoords() throws IOException {
for (int x = -96; x < 0; x++) { for (int x = -96; x < 0; x++) {
@ -175,15 +204,26 @@ public class ChunkStoreTest {
} }
private void assertEqual(ChunkStore expected, ChunkStore actual) private void assertEqual(ChunkStore expected, ChunkStore actual)
{
Assert.assertEquals(expected.getChunkMin(), actual.getChunkMin());
Assert.assertEquals(expected.getChunkMax(), actual.getChunkMax());
assertEqualIgnoreMinMax(expected, actual);
}
private void assertEqualIgnoreMinMax(ChunkStore expected, ChunkStore actual)
{ {
Assert.assertEquals(expected.getChunkX(), actual.getChunkX()); Assert.assertEquals(expected.getChunkX(), actual.getChunkX());
Assert.assertEquals(expected.getChunkZ(), actual.getChunkZ()); Assert.assertEquals(expected.getChunkZ(), actual.getChunkZ());
Assert.assertEquals(expected.getWorldId(), actual.getWorldId()); Assert.assertEquals(expected.getWorldId(), actual.getWorldId());
for (int y = 0; y < 256; y++) for (int y = Math.min(actual.getChunkMin(), expected.getChunkMin()); y < Math.max(actual.getChunkMax(), expected.getChunkMax()); y++)
{
if (expected.getChunkMin() > y || actual.getChunkMin() > y || expected.getChunkMax() <= y || actual.getChunkMax() <= y)
continue; // Ignore
for (int x = 0; x < 16; x++) for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++) for (int z = 0; z < 16; z++)
Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z)); Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
} }
}
private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException { private static byte[] serializeChunkstore(@NotNull ChunkStore chunkStore) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@ -231,6 +271,16 @@ public class ChunkStoreTest {
return cz; return cz;
} }
@Override
public int getChunkMin() {
return 0;
}
@Override
public int getChunkMax() {
return store[0][0].length;
}
@Override @Override
public @NotNull UUID getWorldId() { public @NotNull UUID getWorldId() {
return worldUid; return worldUid;