new chunkstore

Co-authored-by: t00thpick1 <t00thpick1dirko@gmail.com>
This commit is contained in:
nossr50 2020-12-30 15:20:24 -08:00
parent 7802d54ebd
commit 01f31e76f5
28 changed files with 1211 additions and 2406 deletions

View File

@ -1,3 +1,10 @@
Version 2.1.165
The mcMMO system which tracks player placed blocks has had some major rewrites (thanks t00thpick1)
mcMMO will now be compatible with changes to world height (1.17 compatibility)
NOTES:
t00thpick1 has taken time to rewrite our block meta tracking system to be more efficient, easier to maintain, and support upcoming features such as world height changes
Version 2.1.164 Version 2.1.164
mcMMO will now let players use vanilla blocks that have interactions (such as the vanilla Anvil) which are assigned as either Repair or Salvage blocks if a player is sneaking (see notes) mcMMO will now let players use vanilla blocks that have interactions (such as the vanilla Anvil) which are assigned as either Repair or Salvage blocks if a player is sneaking (see notes)
The Rarity known as Records has been renamed to Mythic The Rarity known as Records has been renamed to Mythic

22
pom.xml
View File

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.nossr50.mcMMO</groupId> <groupId>com.gmail.nossr50.mcMMO</groupId>
<artifactId>mcMMO</artifactId> <artifactId>mcMMO</artifactId>
<version>2.1.164</version> <version>2.1.165-SNAPSHOT</version>
<name>mcMMO</name> <name>mcMMO</name>
<url>https://github.com/mcMMO-Dev/mcMMO</url> <url>https://github.com/mcMMO-Dev/mcMMO</url>
<scm> <scm>
@ -279,7 +279,25 @@
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit-dep</artifactId> <artifactId>junit-dep</artifactId>
<version>4.10</version> <version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.4.6</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>

View File

@ -42,28 +42,6 @@ public class WorldListener implements Listener {
} }
} }
/**
* Monitor WorldInit events.
*
* @param event The event to watch
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onWorldInit(WorldInitEvent event) {
/* WORLD BLACKLIST CHECK */
if(WorldBlacklist.isWorldBlacklisted(event.getWorld()))
return;
World world = event.getWorld();
if (!new File(world.getWorldFolder(), "mcmmo_data").exists() || plugin == null) {
return;
}
plugin.getLogger().info("Converting block storage for " + world.getName() + " to a new format.");
//new BlockStoreConversionMain(world).run();
}
/** /**
* Monitor WorldUnload events. * Monitor WorldUnload events.
* *

View File

@ -38,8 +38,8 @@ import com.gmail.nossr50.skills.salvage.salvageables.Salvageable;
import com.gmail.nossr50.skills.salvage.salvageables.SalvageableManager; import com.gmail.nossr50.skills.salvage.salvageables.SalvageableManager;
import com.gmail.nossr50.skills.salvage.salvageables.SimpleSalvageableManager; import com.gmail.nossr50.skills.salvage.salvageables.SimpleSalvageableManager;
import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.*;
import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManager; import com.gmail.nossr50.util.blockmeta.ChunkManager;
import com.gmail.nossr50.util.blockmeta.chunkmeta.ChunkManagerFactory; import com.gmail.nossr50.util.blockmeta.ChunkManagerFactory;
import com.gmail.nossr50.util.commands.CommandRegistrationManager; import com.gmail.nossr50.util.commands.CommandRegistrationManager;
import com.gmail.nossr50.util.compat.CompatibilityManager; import com.gmail.nossr50.util.compat.CompatibilityManager;
import com.gmail.nossr50.util.experience.FormulaManager; import com.gmail.nossr50.util.experience.FormulaManager;
@ -336,8 +336,8 @@ public class mcMMO extends JavaPlugin {
formulaManager.saveFormula(); formulaManager.saveFormula();
holidayManager.saveAnniversaryFiles(); holidayManager.saveAnniversaryFiles();
placeStore.saveAll(); // Save our metadata
placeStore.cleanUp(); // Cleanup empty metadata stores placeStore.cleanUp(); // Cleanup empty metadata stores
placeStore.closeAll();
} }
catch (Exception e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); }

View File

@ -0,0 +1,243 @@
package com.gmail.nossr50.util.blockmeta;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.*;
import java.util.BitSet;
import java.util.UUID;
public class BitSetChunkStore implements ChunkStore, Serializable {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
// 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 BitSet store;
private static final int CURRENT_VERSION = 8;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private int cx;
private int cz;
private int worldHeight;
private UUID worldUid;
public BitSetChunkStore(World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.worldHeight = world.getMaxHeight();
this.store = new BitSet(16 * 16 * worldHeight);
}
private BitSetChunkStore() {}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public UUID getWorldId() {
return worldUid;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store.get(coordToIndex(x, y, z));
}
@Override
public void setTrue(int x, int y, int z) {
set(x, y, z, true);
}
@Override
public void setFalse(int x, int y, int z) {
set(x, y, z, false);
}
@Override
public void set(int x, int y, int z, boolean value) {
store.set(coordToIndex(x, y, z), value);
dirty = true;
}
@Override
public boolean isEmpty() {
return store.isEmpty();
}
private int coordToIndex(int x, int y, int z) {
if (x < 0 || x >= 16 || y < 0 || y >= worldHeight || z < 0 || z >= 16)
throw new IndexOutOfBoundsException();
return (z * 16 + x) + (256 * y);
}
private void fixWorldHeight() {
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;
// Lop off any extra data if the world height has shrunk
int currentWorldHeight = world.getMaxHeight();
if (currentWorldHeight < worldHeight)
{
store.clear(coordToIndex(16, currentWorldHeight, 16), store.length());
worldHeight = currentWorldHeight;
dirty = true;
}
// If the world height has grown, update the worldHeight variable, but don't bother marking it dirty as unless something else changes we don't need to force a file write;
else if (currentWorldHeight > worldHeight)
worldHeight = currentWorldHeight;
}
@Deprecated
private void writeObject(ObjectOutputStream out) throws IOException {
throw new UnsupportedOperationException("Serializable support should only be used for legacy deserialization");
}
@Deprecated
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.readInt(); // Magic number
in.readInt(); // Format version
long lsb = in.readLong();
long msb = in.readLong();
worldUid = new UUID(msb, lsb);
cx = in.readInt();
cz = in.readInt();
boolean[][][] oldStore = (boolean[][][]) in.readObject();
worldHeight = oldStore[0][0].length;
store = new BitSet(16 * 16 * worldHeight / 8);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < worldHeight; y++) {
store.set(coordToIndex(x, y, z), oldStore[x][z][y]);
}
}
}
dirty = true;
fixWorldHeight();
}
private void serialize(DataOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeInt(worldHeight);
// Store the byte array directly so we don't have the object type info overhead
byte[] storeData = store.toByteArray();
out.writeInt(storeData.length);
out.write(storeData);
dirty = false;
}
private static BitSetChunkStore deserialize(DataInputStream in) throws IOException {
int magic = in.readInt();
// Can be used to determine the format of the file
int fileVersionNumber = in.readInt();
if (magic != MAGIC_NUMBER || fileVersionNumber != CURRENT_VERSION)
throw new IOException();
BitSetChunkStore chunkStore = new BitSetChunkStore();
long lsb = in.readLong();
long msb = in.readLong();
chunkStore.worldUid = new UUID(msb, lsb);
chunkStore.cx = in.readInt();
chunkStore.cz = in.readInt();
chunkStore.worldHeight = in.readInt();
byte[] temp = new byte[in.readInt()];
in.readFully(temp);
chunkStore.store = BitSet.valueOf(temp);
chunkStore.fixWorldHeight();
return chunkStore;
}
public static class Serialization {
public static final short STREAM_MAGIC = (short)0xACDC;
public static ChunkStore readChunkStore(DataInputStream inputStream) throws IOException {
if (inputStream.markSupported())
inputStream.mark(2);
short magicNumber = inputStream.readShort();
if (magicNumber == ObjectStreamConstants.STREAM_MAGIC) // Java serializable, use legacy serialization
{
// "Un-read" the magic number for Serializables, they need it to still be in the stream
if (inputStream.markSupported())
inputStream.reset(); // Pretend we never read those bytes
else
{
// Creates a new stream with the two magic number bytes and then the rest of the original stream... Java is so dumb. I just wanted to look at two bytes.
PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream, 2);
pushbackInputStream.unread((magicNumber >>> 0) & 0xFF);
pushbackInputStream.unread((magicNumber >>> 8) & 0xFF);
inputStream = new DataInputStream(pushbackInputStream);
}
return new LegacyDeserializationInputStream(inputStream).readLegacyChunkStore();
}
else if (magicNumber == STREAM_MAGIC) // Pure bytes format
{
return BitSetChunkStore.deserialize(inputStream);
}
throw new IOException("Bad Data Format");
}
public static void writeChunkStore(DataOutputStream outputStream, ChunkStore chunkStore) throws IOException {
if (!(chunkStore instanceof BitSetChunkStore))
throw new InvalidClassException("ChunkStore must be instance of BitSetChunkStore");
outputStream.writeShort(STREAM_MAGIC);
((BitSetChunkStore)chunkStore).serialize(outputStream);
}
// Handles loading the old serialized classes even though we have changed name/package
private static class LegacyDeserializationInputStream extends ObjectInputStream {
public LegacyDeserializationInputStream(InputStream in) throws IOException {
super(in);
enableResolveObject(true);
}
@Override
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass read = super.readClassDescriptor();
if (read.getName().contentEquals("com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore"))
return ObjectStreamClass.lookup(BitSetChunkStore.class);
return read;
}
public ChunkStore readLegacyChunkStore(){
try {
return (ChunkStore) readObject();
} catch (IOException | ClassNotFoundException e) {
return null;
}
}
}
}
}

View File

@ -1,59 +1,12 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta; package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockState; import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import java.io.IOException;
public interface ChunkManager { public interface ChunkManager {
void closeAll(); void closeAll();
ChunkStore readChunkStore(World world, int x, int z) throws IOException;
void writeChunkStore(World world, int x, int z, ChunkStore data);
void closeChunkStore(World world, int x, int z);
/**
* Loads a specific chunklet
*
* @param cx Chunklet X coordinate that needs to be loaded
* @param cy Chunklet Y coordinate that needs to be loaded
* @param cz Chunklet Z coordinate that needs to be loaded
* @param world World that the chunklet needs to be loaded in
*/
void loadChunklet(int cx, int cy, int cz, World world);
/**
* Unload a specific chunklet
*
* @param cx Chunklet X coordinate that needs to be unloaded
* @param cy Chunklet Y coordinate that needs to be unloaded
* @param cz Chunklet Z coordinate that needs to be unloaded
* @param world World that the chunklet needs to be unloaded from
*/
void unloadChunklet(int cx, int cy, int cz, World world);
/**
* Load a given Chunk's Chunklet data
*
* @param cx Chunk X coordinate that is to be loaded
* @param cz Chunk Z coordinate that is to be loaded
* @param world World that the Chunk is in
*/
void loadChunk(int cx, int cz, World world, Entity[] entities);
/**
* Unload a given Chunk's Chunklet data
*
* @param cx Chunk X coordinate that is to be unloaded
* @param cz Chunk Z coordinate that is to be unloaded
* @param world World that the Chunk is in
*/
void unloadChunk(int cx, int cz, World world);
/** /**
* Saves a given Chunk's Chunklet data * Saves a given Chunk's Chunklet data
* *
@ -63,17 +16,6 @@ public interface ChunkManager {
*/ */
void saveChunk(int cx, int cz, World world); void saveChunk(int cx, int cz, World world);
boolean isChunkLoaded(int cx, int cz, World world);
/**
* Informs the ChunkletManager a chunk is loaded
*
* @param cx Chunk X coordinate that is loaded
* @param cz Chunk Z coordinate that is loaded
* @param world World that the chunk was loaded in
*/
void chunkLoaded(int cx, int cz, World world);
/** /**
* Informs the ChunkletManager a chunk is unloaded * Informs the ChunkletManager a chunk is unloaded
* *
@ -97,23 +39,11 @@ public interface ChunkManager {
*/ */
void unloadWorld(World world); void unloadWorld(World world);
/**
* Load all ChunkletStores from all loaded chunks from this world into memory
*
* @param world World to load
*/
void loadWorld(World world);
/** /**
* Save all ChunkletStores * Save all ChunkletStores
*/ */
void saveAll(); void saveAll();
/**
* Unload all ChunkletStores after saving them
*/
void unloadAll();
/** /**
* Check to see if a given location is set to true * Check to see if a given location is set to true
* *

View File

@ -1,4 +1,4 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta; package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.config.HiddenConfig; import com.gmail.nossr50.config.HiddenConfig;

View File

@ -1,13 +1,13 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta; package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.util.blockmeta.ChunkletStore; import org.bukkit.World;
import java.io.Serializable; import java.util.UUID;
/** /**
* A ChunkStore should be responsible for a 16x16xWorldHeight area of data * A ChunkStore should be responsible for a 16x16xWorldHeight area of data
*/ */
public interface ChunkStore extends Serializable { public interface ChunkStore {
/** /**
* Checks the chunk's save state * Checks the chunk's save state
* *
@ -36,6 +36,8 @@ public interface ChunkStore extends Serializable {
*/ */
int getChunkZ(); int getChunkZ();
UUID getWorldId();
/** /**
* Checks the value at the given coordinates * Checks the value at the given coordinates
* *
@ -64,15 +66,18 @@ public interface ChunkStore extends Serializable {
*/ */
void setFalse(int x, int y, int z); void setFalse(int x, int y, int z);
/**
* Set the value at the given coordinates
*
* @param x x coordinate in current chunklet
* @param y y coordinate in current chunklet
* @param z z coordinate in current chunklet
* @param value value to set
*/
void set(int x, int y, int z, boolean value);
/** /**
* @return true if all values in the chunklet are false, false if otherwise * @return true if all values in the chunklet are false, false if otherwise
*/ */
boolean isEmpty(); boolean isEmpty();
/**
* Set all values in this ChunkletStore to the values from another provided ChunkletStore
*
* @param otherStore Another ChunkletStore that this one should copy all data from
*/
void copyFrom(ChunkletStore otherStore);
} }

View File

@ -1,151 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World;
import org.bukkit.block.Block;
public interface ChunkletManager {
/**
* Loads a specific chunklet
*
* @param cx Chunklet X coordinate that needs to be loaded
* @param cy Chunklet Y coordinate that needs to be loaded
* @param cz Chunklet Z coordinate that needs to be loaded
* @param world World that the chunklet needs to be loaded in
*/
void loadChunklet(int cx, int cy, int cz, World world);
/**
* Unload a specific chunklet
*
* @param cx Chunklet X coordinate that needs to be unloaded
* @param cy Chunklet Y coordinate that needs to be unloaded
* @param cz Chunklet Z coordinate that needs to be unloaded
* @param world World that the chunklet needs to be unloaded from
*/
void unloadChunklet(int cx, int cy, int cz, World world);
/**
* Load a given Chunk's Chunklet data
*
* @param cx Chunk X coordinate that is to be loaded
* @param cz Chunk Z coordinate that is to be loaded
* @param world World that the Chunk is in
*/
void loadChunk(int cx, int cz, World world);
/**
* Unload a given Chunk's Chunklet data
*
* @param cx Chunk X coordinate that is to be unloaded
* @param cz Chunk Z coordinate that is to be unloaded
* @param world World that the Chunk is in
*/
void unloadChunk(int cx, int cz, World world);
/**
* Informs the ChunkletManager a chunk is loaded
*
* @param cx Chunk X coordinate that is loaded
* @param cz Chunk Z coordinate that is loaded
* @param world World that the chunk was loaded in
*/
void chunkLoaded(int cx, int cz, World world);
/**
* Informs the ChunkletManager a chunk is unloaded
*
* @param cx Chunk X coordinate that is unloaded
* @param cz Chunk Z coordinate that is unloaded
* @param world World that the chunk was unloaded in
*/
void chunkUnloaded(int cx, int cz, World world);
/**
* Save all ChunkletStores related to the given world
*
* @param world World to save
*/
void saveWorld(World world);
/**
* Unload all ChunkletStores from memory related to the given world after saving them
*
* @param world World to unload
*/
void unloadWorld(World world);
/**
* Load all ChunkletStores from all loaded chunks from this world into memory
*
* @param world World to load
*/
void loadWorld(World world);
/**
* Save all ChunkletStores
*/
void saveAll();
/**
* Unload all ChunkletStores after saving them
*/
void unloadAll();
/**
* Check to see if a given location is set to true
*
* @param x X coordinate to check
* @param y Y coordinate to check
* @param z Z coordinate to check
* @param world World to check in
* @return true if the given location is set to true, false if otherwise
*/
boolean isTrue(int x, int y, int z, World world);
/**
* Check to see if a given block location is set to true
*
* @param block Block location to check
* @return true if the given block location is set to true, false if otherwise
*/
boolean isTrue(Block block);
/**
* Set a given location to true, should create stores as necessary if the location does not exist
*
* @param x X coordinate to set
* @param y Y coordinate to set
* @param z Z coordinate to set
* @param world World to set in
*/
void setTrue(int x, int y, int z, World world);
/**
* Set a given block location to true, should create stores as necessary if the location does not exist
*
* @param block Block location to set
*/
void setTrue(Block block);
/**
* Set a given location to false, should not create stores if one does not exist for the given location
*
* @param x X coordinate to set
* @param y Y coordinate to set
* @param z Z coordinate to set
* @param world World to set in
*/
void setFalse(int x, int y, int z, World world);
/**
* Set a given block location to false, should not create stores if one does not exist for the given location
*
* @param block Block location to set
*/
void setFalse(Block block);
/**
* Delete any ChunkletStores that are empty
*/
void cleanUp();
}

View File

@ -1,15 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.config.HiddenConfig;
public class ChunkletManagerFactory {
public static ChunkletManager getChunkletManager() {
HiddenConfig hConfig = HiddenConfig.getInstance();
if (hConfig.getChunkletsEnabled()) {
return new HashChunkletManager();
}
return new NullChunkletManager();
}
}

View File

@ -1,48 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
import java.io.Serializable;
/**
* A ChunkletStore should be responsible for a 16x16x64 area of data
*/
public interface ChunkletStore extends Serializable {
/**
* Checks the value at the given coordinates
*
* @param x x coordinate in current chunklet
* @param y y coordinate in current chunklet
* @param z z coordinate in current chunklet
* @return true if the value is true at the given coordinates, false if otherwise
*/
boolean isTrue(int x, int y, int z);
/**
* Set the value to true at the given coordinates
*
* @param x x coordinate in current chunklet
* @param y y coordinate in current chunklet
* @param z z coordinate in current chunklet
*/
void setTrue(int x, int y, int z);
/**
* Set the value to false at the given coordinates
*
* @param x x coordinate in current chunklet
* @param y y coordinate in current chunklet
* @param z z coordinate in current chunklet
*/
void setFalse(int x, int y, int z);
/**
* @return true if all values in the chunklet are false, false if otherwise
*/
boolean isEmpty();
/**
* Set all values in this ChunkletStore to the values from another provided ChunkletStore
*
* @param otherStore Another ChunkletStore that this one should copy all data from
*/
void copyFrom(ChunkletStore otherStore);
}

View File

@ -1,8 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
public class ChunkletStoreFactory {
protected static ChunkletStore getChunkletStore() {
// TODO: Add in loading from config what type of store we want.
return new PrimitiveExChunkletStore();
}
}

View File

@ -0,0 +1,354 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import java.io.*;
import java.util.*;
public class HashChunkManager implements ChunkManager {
private final HashMap<CoordinateKey, McMMOSimpleRegionFile> regionMap = new HashMap<>(); // Tracks active regions
private final HashMap<CoordinateKey, HashSet<CoordinateKey>> chunkUsageMap = new HashMap<>(); // Tracks active chunks by region
private final HashMap<CoordinateKey, ChunkStore> chunkMap = new HashMap<>(); // Tracks active chunks
@Override
public synchronized void closeAll() {
// Save all dirty chunkstores
for (ChunkStore chunkStore : chunkMap.values())
{
if (!chunkStore.isDirty())
continue;
writeChunkStore(Bukkit.getWorld(chunkStore.getWorldId()), chunkStore);
}
// Clear in memory chunks
chunkMap.clear();
chunkUsageMap.clear();
// Close all region files
for (McMMOSimpleRegionFile rf : regionMap.values())
rf.close();
regionMap.clear();
}
private synchronized ChunkStore readChunkStore(World world, int cx, int cz) throws IOException {
McMMOSimpleRegionFile rf = getSimpleRegionFile(world, cx, cz, false);
if (rf == null)
return null; // If there is no region file, there can't be a chunk
try (DataInputStream in = rf.getInputStream(cx, cz)) { // Get input stream for chunk
if (in == null)
return null; // No chunk
return BitSetChunkStore.Serialization.readChunkStore(in); // Read in the chunkstore
}
}
private synchronized void writeChunkStore(World world, ChunkStore data) {
if (!data.isDirty())
return; // Don't save unchanged data
try {
McMMOSimpleRegionFile rf = getSimpleRegionFile(world, data.getChunkX(), data.getChunkZ(), true);
try (DataOutputStream out = rf.getOutputStream(data.getChunkX(), data.getChunkZ())) {
BitSetChunkStore.Serialization.writeChunkStore(out, data);
}
data.setDirty(false);
}
catch (IOException e) {
throw new RuntimeException("Unable to write chunk meta data for " + data.getChunkX() + ", " + data.getChunkZ(), e);
}
}
private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int cx, int cz, boolean createIfAbsent) {
CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
return regionMap.computeIfAbsent(regionKey, k -> {
File worldRegionsDirectory = new File(world.getWorldFolder(), "mcmmo_regions");
if (!createIfAbsent && !worldRegionsDirectory.isDirectory())
return null; // Don't create the directory on read-only operations
worldRegionsDirectory.mkdirs(); // Ensure directory exists
File regionFile = new File(worldRegionsDirectory, "mcmmo_" + regionKey.x + "_" + regionKey.z + "_.mcm");
if (!createIfAbsent && !regionFile.exists())
return null; // Don't create the file on read-only operations
return new McMMOSimpleRegionFile(regionFile, regionKey.x, regionKey.z);
});
}
private ChunkStore loadChunk(int cx, int cz, World world) {
try {
return readChunkStore(world, cx, cz);
}
catch (Exception ignored) {}
return null;
}
private void unloadChunk(int cx, int cz, World world) {
CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
ChunkStore chunkStore = chunkMap.remove(chunkKey); // Remove from chunk map
if (chunkStore == null)
return;
if (chunkStore.isDirty())
writeChunkStore(world, chunkStore);
CoordinateKey regionKey = toRegionKey(world.getUID(), cx, cz);
HashSet<CoordinateKey> chunkKeys = chunkUsageMap.get(regionKey);
chunkKeys.remove(chunkKey); // remove from region file in-use set
if (chunkKeys.isEmpty()) // If it was last chunk in region, close the region file and remove it from memory
{
chunkUsageMap.remove(regionKey);
regionMap.remove(regionKey).close();
}
}
@Override
public synchronized void saveChunk(int cx, int cz, World world) {
if (world == null)
return;
CoordinateKey chunkKey = toChunkKey(world.getUID(), cx, cz);
ChunkStore out = chunkMap.get(chunkKey);
if (out == null)
return;
if (!out.isDirty())
return;
writeChunkStore(world, out);
}
@Override
public synchronized void chunkUnloaded(int cx, int cz, World world) {
if (world == null)
return;
unloadChunk(cx, cz, world);
}
@Override
public synchronized void saveWorld(World world) {
if (world == null)
return;
UUID wID = world.getUID();
// Save all teh chunks
for (ChunkStore chunkStore : chunkMap.values()) {
if (!chunkStore.isDirty())
continue;
if (!wID.equals(chunkStore.getWorldId()))
continue;
try {
writeChunkStore(world, chunkStore);
}
catch (Exception ignore) { }
}
}
@Override
public synchronized void unloadWorld(World world) {
if (world == null)
return;
UUID wID = world.getUID();
// Save and remove all the chunks
List<CoordinateKey> chunkKeys = new ArrayList<>(chunkMap.keySet());
for (CoordinateKey chunkKey : chunkKeys) {
if (!wID.equals(chunkKey.worldID))
continue;
ChunkStore chunkStore = chunkMap.remove(chunkKey);
if (!chunkStore.isDirty())
continue;
try {
writeChunkStore(world, chunkStore);
}
catch (Exception ignore) { }
}
// Clear all the region files
List<CoordinateKey> regionKeys = new ArrayList<>(regionMap.keySet());
for (CoordinateKey regionKey : regionKeys) {
if (!wID.equals(regionKey.worldID))
continue;
regionMap.remove(regionKey).close();
chunkUsageMap.remove(regionKey);
}
}
@Override
public synchronized void saveAll() {
for (World world : mcMMO.p.getServer().getWorlds()) {
saveWorld(world);
}
}
@Override
public synchronized boolean isTrue(int x, int y, int z, World world) {
if (world == null)
return false;
CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
// Get chunk, load from file if necessary
// Get/Load/Create chunkstore
ChunkStore check = chunkMap.computeIfAbsent(chunkKey, k -> {
// Load from file
ChunkStore loaded = loadChunk(chunkKey.x, chunkKey.z, world);
if (loaded == null)
return null;
// Mark chunk in-use for region tracking
chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
return loaded;
});
// No chunk, return false
if (check == null)
return false;
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
return check.isTrue(ix, y, iz);
}
@Override
public synchronized boolean isTrue(Block block) {
if (block == null)
return false;
return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public synchronized boolean isTrue(BlockState blockState) {
if (blockState == null)
return false;
return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
@Override
public synchronized void setTrue(int x, int y, int z, World world) {
set(x, y, z, world, true);
}
@Override
public synchronized void setTrue(Block block) {
if (block == null)
return;
setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public synchronized void setTrue(BlockState blockState) {
if (blockState == null)
return;
setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
@Override
public synchronized void setFalse(int x, int y, int z, World world) {
set(x, y, z, world, false);
}
@Override
public synchronized void setFalse(Block block) {
if (block == null)
return;
setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public synchronized void setFalse(BlockState blockState) {
if (blockState == null)
return;
setFalse(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
public synchronized void set(int x, int y, int z, World world, boolean value){
if (world == null)
return;
CoordinateKey chunkKey = blockCoordinateToChunkKey(world.getUID(), x, y, z);
// Get/Load/Create chunkstore
ChunkStore cStore = chunkMap.computeIfAbsent(chunkKey, k -> {
// Load from file
ChunkStore loaded = loadChunk(chunkKey.x, chunkKey.z, world);
if (loaded != null)
{
chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
return loaded;
}
// If setting to false, no need to create an empty chunkstore
if (!value)
return null;
// Mark chunk in-use for region tracking
chunkUsageMap.computeIfAbsent(toRegionKey(chunkKey.worldID, chunkKey.x, chunkKey.z), j -> new HashSet<>()).add(chunkKey);
// Create a new chunkstore
return new BitSetChunkStore(world, chunkKey.x, chunkKey.z);
});
// Indicates setting false on empty chunkstore
if (cStore == null)
return;
// Get block offset (offset from chunk corner)
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
// Set chunk store value
cStore.set(ix, y, iz, value);
}
private CoordinateKey blockCoordinateToChunkKey(UUID worldUid, int x, int y, int z) {
return toChunkKey(worldUid, x >> 4, z >> 4);
}
private CoordinateKey toChunkKey(UUID worldUid, int cx, int cz){
return new CoordinateKey(worldUid, cx, cz);
}
private CoordinateKey toRegionKey(UUID worldUid, int cx, int cz) {
// Compute region index (32x32 chunk regions)
int rx = cx >> 5;
int rz = cz >> 5;
return new CoordinateKey(worldUid, rx, rz);
}
private static final class CoordinateKey {
public final UUID worldID;
public final int x;
public final int z;
private CoordinateKey(UUID worldID, int x, int z) {
this.worldID = worldID;
this.x = x;
this.z = z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CoordinateKey coordinateKey = (CoordinateKey) o;
return x == coordinateKey.x &&
z == coordinateKey.z &&
worldID.equals(coordinateKey.worldID);
}
@Override
public int hashCode() {
return Objects.hash(worldID, x, z);
}
}
@Override
public synchronized void cleanUp() {}
}

View File

@ -1,410 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
import com.gmail.nossr50.mcMMO;
import org.bukkit.World;
import org.bukkit.block.Block;
import java.io.*;
import java.util.HashMap;
public class HashChunkletManager implements ChunkletManager {
public HashMap<String, ChunkletStore> store = new HashMap<>();
@Override
public void loadChunklet(int cx, int cy, int cz, World world) {
File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
File cxDir = new File(dataDir, "" + cx);
if (!cxDir.exists()) {
return;
}
File czDir = new File(cxDir, "" + cz);
if (!czDir.exists()) {
return;
}
File yFile = new File(czDir, "" + cy);
if (!yFile.exists()) {
return;
}
ChunkletStore in = deserializeChunkletStore(yFile);
if (in != null) {
store.put(world.getName() + "," + cx + "," + cz + "," + cy, in);
}
}
@Override
public void unloadChunklet(int cx, int cy, int cz, World world) {
File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
if (store.containsKey(world.getName() + "," + cx + "," + cz + "," + cy)) {
File cxDir = new File(dataDir, "" + cx);
if (!cxDir.exists()) {
cxDir.mkdir();
}
File czDir = new File(cxDir, "" + cz);
if (!czDir.exists()) {
czDir.mkdir();
}
File yFile = new File(czDir, "" + cy);
ChunkletStore out = store.get(world.getName() + "," + cx + "," + cz + "," + cy);
serializeChunkletStore(out, yFile);
store.remove(world.getName() + "," + cx + "," + cz + "," + cy);
}
}
@Override
public void loadChunk(int cx, int cz, World world) {
File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
File cxDir = new File(dataDir, "" + cx);
if (!cxDir.exists()) {
return;
}
File czDir = new File(cxDir, "" + cz);
if (!czDir.exists()) {
return;
}
for (int y = 0; y < 4; y++) {
File yFile = new File(czDir, "" + y);
if (!yFile.exists()) {
continue;
}
ChunkletStore in = deserializeChunkletStore(yFile);
if (in != null) {
store.put(world.getName() + "," + cx + "," + cz + "," + y, in);
}
}
}
@Override
public void unloadChunk(int cx, int cz, World world) {
File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
for (int y = 0; y < 4; y++) {
if (store.containsKey(world.getName() + "," + cx + "," + cz + "," + y)) {
File cxDir = new File(dataDir, "" + cx);
if (!cxDir.exists()) {
cxDir.mkdir();
}
File czDir = new File(cxDir, "" + cz);
if (!czDir.exists()) {
czDir.mkdir();
}
File yFile = new File(czDir, "" + y);
ChunkletStore out = store.get(world.getName() + "," + cx + "," + cz + "," + y);
serializeChunkletStore(out, yFile);
store.remove(world.getName() + "," + cx + "," + cz + "," + y);
}
}
}
@Override
public void chunkLoaded(int cx, int cz, World world) {
//loadChunk(cx, cz, world);
}
@Override
public void chunkUnloaded(int cx, int cz, World world) {
unloadChunk(cx, cx, world);
}
@Override
public void saveWorld(World world) {
String worldName = world.getName();
File dataDir = new File(world.getWorldFolder(), "mcmmo_data");
if (!dataDir.exists()) {
dataDir.mkdirs();
}
for (String key : store.keySet()) {
String[] info = key.split(",");
if (worldName.equals(info[0])) {
File cxDir = new File(dataDir, "" + info[1]);
if (!cxDir.exists()) {
cxDir.mkdir();
}
File czDir = new File(cxDir, "" + info[2]);
if (!czDir.exists()) {
czDir.mkdir();
}
File yFile = new File(czDir, "" + info[3]);
serializeChunkletStore(store.get(key), yFile);
}
}
}
@Override
public void unloadWorld(World world) {
saveWorld(world);
String worldName = world.getName();
for (String key : store.keySet()) {
String tempWorldName = key.split(",")[0];
if (tempWorldName.equals(worldName)) {
store.remove(key);
return;
}
}
}
@Override
public void loadWorld(World world) {
//for (Chunk chunk : world.getLoadedChunks()) {
// this.chunkLoaded(chunk.getX(), chunk.getZ(), world);
//}
}
@Override
public void saveAll() {
for (World world : mcMMO.p.getServer().getWorlds()) {
saveWorld(world);
}
}
@Override
public void unloadAll() {
saveAll();
for (World world : mcMMO.p.getServer().getWorlds()) {
unloadWorld(world);
}
}
@Override
public boolean isTrue(int x, int y, int z, World world) {
int cx = x >> 4;
int cz = z >> 4;
int cy = y >> 6;
String key = world.getName() + "," + cx + "," + cz + "," + cy;
if (!store.containsKey(key)) {
loadChunklet(cx, cy, cz, world);
}
if (!store.containsKey(key)) {
return false;
}
ChunkletStore check = store.get(world.getName() + "," + cx + "," + cz + "," + cy);
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
int iy = Math.abs(y) % 64;
return check.isTrue(ix, iy, iz);
}
@Override
public boolean isTrue(Block block) {
return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public void setTrue(int x, int y, int z, World world) {
int cx = x >> 4;
int cz = z >> 4;
int cy = y >> 6;
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
int iy = Math.abs(y) % 64;
String key = world.getName() + "," + cx + "," + cz + "," + cy;
if (!store.containsKey(key)) {
loadChunklet(cx, cy, cz, world);
}
ChunkletStore cStore = store.get(key);
if (cStore == null) {
cStore = ChunkletStoreFactory.getChunkletStore();
store.put(world.getName() + "," + cx + "," + cz + "," + cy, cStore);
}
cStore.setTrue(ix, iy, iz);
}
@Override
public void setTrue(Block block) {
setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public void setFalse(int x, int y, int z, World world) {
int cx = x >> 4;
int cz = z >> 4;
int cy = y >> 6;
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
int iy = Math.abs(y) % 64;
String key = world.getName() + "," + cx + "," + cz + "," + cy;
if (!store.containsKey(key)) {
loadChunklet(cx, cy, cz, world);
}
ChunkletStore cStore = store.get(key);
if (cStore == null) {
return; // No need to make a store for something we will be setting to false
}
cStore.setFalse(ix, iy, iz);
}
@Override
public void setFalse(Block block) {
setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public void cleanUp() {
for (String key : store.keySet()) {
if (store.get(key).isEmpty()) {
String[] info = key.split(",");
File dataDir = new File(mcMMO.p.getServer().getWorld(info[0]).getWorldFolder(), "mcmmo_data");
File cxDir = new File(dataDir, "" + info[1]);
if (!cxDir.exists()) {
continue;
}
File czDir = new File(cxDir, "" + info[2]);
if (!czDir.exists()) {
continue;
}
File yFile = new File(czDir, "" + info[3]);
yFile.delete();
// Delete empty directories
if (czDir.list().length == 0) {
czDir.delete();
}
if (cxDir.list().length == 0) {
cxDir.delete();
}
}
}
}
/**
* @param cStore ChunkletStore to save
* @param location Where on the disk to put it
*/
private void serializeChunkletStore(ChunkletStore cStore, File location) {
FileOutputStream fileOut = null;
ObjectOutputStream objOut = null;
try {
if (!location.exists()) {
location.createNewFile();
}
fileOut = new FileOutputStream(location);
objOut = new ObjectOutputStream(fileOut);
objOut.writeObject(cStore);
}
catch (IOException ex) {
ex.printStackTrace();
}
finally {
if (objOut != null) {
try {
objOut.flush();
objOut.close();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
if (fileOut != null) {
try {
fileOut.close();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
/**
* @param location Where on the disk to read from
* @return ChunkletStore from the specified location
*/
private ChunkletStore deserializeChunkletStore(File location) {
ChunkletStore storeIn = null;
FileInputStream fileIn = null;
ObjectInputStream objIn = null;
try {
fileIn = new FileInputStream(location);
objIn = new ObjectInputStream(new BufferedInputStream(fileIn));
storeIn = (ChunkletStore) objIn.readObject();
}
catch (IOException ex) {
if (ex instanceof EOFException) {
// EOF should only happen on Chunklets that somehow have been corrupted.
//mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " could not be read due to an EOFException, data in this area will be lost.");
return ChunkletStoreFactory.getChunkletStore();
}
else if (ex instanceof StreamCorruptedException) {
// StreamCorrupted happens when the Chunklet is no good.
//mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " is corrupted, data in this area will be lost.");
return ChunkletStoreFactory.getChunkletStore();
}
else if (ex instanceof UTFDataFormatException) {
// UTF happens when the Chunklet cannot be read or is corrupted
//mcMMO.p.getLogger().severe("Chunklet data at " + location.toString() + " could not be read due to an UTFDataFormatException, data in this area will be lost.");
return ChunkletStoreFactory.getChunkletStore();
}
ex.printStackTrace();
}
catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
finally {
if (objIn != null) {
try {
objIn.close();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
if (fileIn != null) {
try {
fileIn.close();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}
// TODO: Make this less messy, as it is, it's kinda... depressing to do it like this.
// Might also make a mess when we move to stacks, but at that point I think I will write a new Manager...
// IMPORTANT! If ChunkletStoreFactory is going to be returning something other than PrimitiveEx we need to remove this, as it will be breaking time for old maps
/*
if (!(storeIn instanceof PrimitiveExChunkletStore)) {
ChunkletStore tempStore = ChunkletStoreFactory.getChunkletStore();
if (storeIn != null) {
tempStore.copyFrom(storeIn);
}
storeIn = tempStore;
}
*/
return storeIn;
}
}

View File

@ -0,0 +1,257 @@
/*
* This file is part of SpoutPlugin.
*
* Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
* SpoutPlugin is licensed under the GNU Lesser General Public License.
*
* SpoutPlugin is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SpoutPlugin is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.gmail.nossr50.util.blockmeta;
import java.io.*;
import java.util.BitSet;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
/**
* File format:
* bytes 0-4096 contain 1024 integer values representing the segment index of each chunk
* bytes 4096-8192 contain 1024 integer values representing the byte length of each chunk
* bytes 8192-8196 is the integer value of the segment exponent
* bytes 8196-12288 are reserved for future use
* bytes 12288+ contain the data segments, by default 1024 byte segments.
* Chunk data is compressed and stored in 1 or more segments as needed.
*/
public class McMMOSimpleRegionFile {
private static final int DEFAULT_SEGMENT_EXPONENT = 10; // TODO, analyze real world usage and determine if a smaller segment(512) is worth it or not. (need to know average chunkstore bytesize)
private static final int DEFAULT_SEGMENT_SIZE = (int)Math.pow(2, DEFAULT_SEGMENT_EXPONENT); // 1024
private static final int RESERVED_HEADER_BYTES = 12288; // This needs to be divisible by segment size
private static final int NUM_CHUNKS = 1024; // 32x32
private static final int SEEK_CHUNK_SEGMENT_INDICES = 0;
private static final int SEEK_CHUNK_BYTE_LENGTHS = 4096;
private static final int SEEK_FILE_INFO = 8192;
// Chunk info
private final int[] chunkSegmentIndex = new int[NUM_CHUNKS];
private final int[] chunkNumBytes = new int[NUM_CHUNKS];
private final int[] chunkNumSegments = new int[NUM_CHUNKS];
// Segments
private final BitSet segments = new BitSet(); // Used to denote which segments are in use or not
// Segment size/mask
private final int segmentExponent;
private final int segmentMask;
// File location
private final File parent;
// File access
private final RandomAccessFile file;
// Region index
private final int rx;
private final int rz;
public McMMOSimpleRegionFile(File f, int rx, int rz) {
this.rx = rx;
this.rz = rz;
this.parent = f;
try {
this.file = new RandomAccessFile(parent, "rw");
// New file, write out header bytes
if (file.length() < RESERVED_HEADER_BYTES) {
file.write(new byte[RESERVED_HEADER_BYTES]);
file.seek(SEEK_FILE_INFO);
file.writeInt(DEFAULT_SEGMENT_EXPONENT);
}
file.seek(SEEK_FILE_INFO);
this.segmentExponent = file.readInt();
this.segmentMask = (1 << segmentExponent) - 1;
// Mark reserved segments reserved
int reservedSegments = this.bytesToSegments(RESERVED_HEADER_BYTES);
segments.set(0, reservedSegments, true);
// Read chunk header data
file.seek(SEEK_CHUNK_SEGMENT_INDICES);
for (int i = 0; i < NUM_CHUNKS; i++)
chunkSegmentIndex[i] = file.readInt();
file.seek(SEEK_CHUNK_BYTE_LENGTHS);
for (int i = 0; i < NUM_CHUNKS; i++) {
chunkNumBytes[i] = file.readInt();
chunkNumSegments[i] = bytesToSegments(chunkNumBytes[i]);
markChunkSegments(i, true);
}
fixFileLength();
}
catch (IOException fnfe) {
throw new RuntimeException(fnfe);
}
}
public synchronized DataOutputStream getOutputStream(int x, int z) {
int index = getChunkIndex(x, z); // Get chunk index
return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
}
private static class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
final McMMOSimpleRegionFile rf;
final int index;
McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
super(DEFAULT_SEGMENT_SIZE);
this.rf = rf;
this.index = index;
}
@Override
public void close() throws IOException {
rf.write(index, buf, count);
}
}
private synchronized void write(int index, byte[] buffer, int size) throws IOException {
int oldSegmentIndex = chunkSegmentIndex[index]; // Get current segment index
markChunkSegments(index, false); // Clear our old segments
int newSegmentIndex = findContiguousSegments(oldSegmentIndex, size); // Find contiguous segments to save to
file.seek(newSegmentIndex << segmentExponent); // Seek to file location
file.write(buffer, 0, size); // Write data
// update in memory info
chunkSegmentIndex[index] = newSegmentIndex;
chunkNumBytes[index] = size;
chunkNumSegments[index] = bytesToSegments(size);
// Mark segments in use
markChunkSegments(index, true);
// Update header info
file.seek(SEEK_CHUNK_SEGMENT_INDICES + (4 * index));
file.writeInt(chunkSegmentIndex[index]);
file.seek(SEEK_CHUNK_BYTE_LENGTHS + (4 * index));
file.writeInt(chunkNumBytes[index]);
}
public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
int index = getChunkIndex(x, z); // Get chunk index
int byteLength = chunkNumBytes[index]; // Get byte length of data
// No bytes
if (byteLength == 0)
return null;
byte[] data = new byte[byteLength];
file.seek(chunkSegmentIndex[index] << segmentExponent); // Seek to file location
file.readFully(data); // Read in the data
return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
}
public synchronized void close() {
try {
file.close();
segments.clear();
}
catch (IOException ioe) {
throw new RuntimeException("Unable to close file", ioe);
}
}
private synchronized void markChunkSegments(int index, boolean inUse) {
// No bytes used
if (chunkNumBytes[index] == 0)
return;
int start = chunkSegmentIndex[index];
int end = start + chunkNumSegments[index];
// If we are writing, assert we don't write over any in-use segments
if (inUse)
{
int nextSetBit = segments.nextSetBit(start);
if (nextSetBit != -1 && nextSetBit < end)
throw new IllegalStateException("Attempting to overwrite an in-use segment");
}
segments.set(start, end, inUse);
}
private synchronized void fixFileLength() throws IOException {
int fileLength = (int)file.length();
int extend = -fileLength & segmentMask; // how many bytes do we need to be divisible by segment size
// Go to end of file
file.seek(fileLength);
// Append bytes
file.write(new byte[extend], 0, extend);
}
private synchronized int findContiguousSegments(int hint, int size) {
if (size == 0)
return 0; // Zero byte data will not claim any chunks anyways
int segments = bytesToSegments(size); // Number of segments we need
// Check the hinted location (previous location of chunk) most of the time we can fit where we were.
boolean oldFree = true;
for (int i = hint; i < this.segments.size() && i < hint + segments; i++) {
if (this.segments.get(i)) {
oldFree = false;
break;
}
}
// We fit!
if (oldFree)
return hint;
// Find somewhere to put us
int start = 0;
int current = 0;
while (current < this.segments.size()) {
boolean segmentInUse = this.segments.get(current); // check if segment is in use
current++; // Move up a segment
// Move up start if the segment was in use
if (segmentInUse)
start = current;
// If we have enough segments now, return
if (current - start >= segments)
return start;
}
// Return the end of the segments (will expand to fit them)
return start;
}
private synchronized int bytesToSegments(int bytes) {
if (bytes <= 0)
return 1;
return ((bytes - 1) >> segmentExponent) + 1; // ((bytes - 1) / segmentSize) + 1
}
private synchronized int getChunkIndex(int x, int z) {
if (rx != (x >> 5) || rz != (z >> 5))
throw new IndexOutOfBoundsException();
x = x & 0x1F; // 5 bits (mod 32)
z = z & 0x1F; // 5 bits (mod 32)
return (x << 5) + z; // x in the upper 5 bits, z in the lower 5 bits
}
}

View File

@ -1,51 +1,17 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta; package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockState; import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import java.io.IOException;
public class NullChunkManager implements ChunkManager { public class NullChunkManager implements ChunkManager {
@Override @Override
public void closeAll() {} public void closeAll() {}
@Override
public ChunkStore readChunkStore(World world, int x, int z) throws IOException {
return null;
}
@Override
public void writeChunkStore(World world, int x, int z, ChunkStore data) {}
@Override
public void closeChunkStore(World world, int x, int z) {}
@Override
public void loadChunklet(int cx, int cy, int cz, World world) {}
@Override
public void unloadChunklet(int cx, int cy, int cz, World world) {}
@Override
public void loadChunk(int cx, int cz, World world, Entity[] entities) {}
@Override
public void unloadChunk(int cx, int cz, World world) {}
@Override @Override
public void saveChunk(int cx, int cz, World world) {} public void saveChunk(int cx, int cz, World world) {}
@Override
public boolean isChunkLoaded(int cx, int cz, World world) {
return true;
}
@Override
public void chunkLoaded(int cx, int cz, World world) {}
@Override @Override
public void chunkUnloaded(int cx, int cz, World world) {} public void chunkUnloaded(int cx, int cz, World world) {}
@ -55,15 +21,9 @@ public class NullChunkManager implements ChunkManager {
@Override @Override
public void unloadWorld(World world) {} public void unloadWorld(World world) {}
@Override
public void loadWorld(World world) {}
@Override @Override
public void saveAll() {} public void saveAll() {}
@Override
public void unloadAll() {}
@Override @Override
public boolean isTrue(int x, int y, int z, World world) { public boolean isTrue(int x, int y, int z, World world) {
return false; return false;

View File

@ -1,85 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
import org.bukkit.World;
import org.bukkit.block.Block;
/**
* A ChunkletManager implementation that does nothing and returns false for all checks.
*
* Useful for turning off Chunklets without actually doing much work
*/
public class NullChunkletManager implements ChunkletManager {
@Override
public void loadChunklet(int cx, int cy, int cz, World world) {
}
@Override
public void unloadChunklet(int cx, int cy, int cz, World world) {
}
@Override
public void loadChunk(int cx, int cz, World world) {
}
@Override
public void unloadChunk(int cx, int cz, World world) {
}
@Override
public void chunkLoaded(int cx, int cz, World world) {
}
@Override
public void chunkUnloaded(int cx, int cz, World world) {
}
@Override
public void saveWorld(World world) {
}
@Override
public void unloadWorld(World world) {
}
@Override
public void loadWorld(World world) {
}
@Override
public void saveAll() {
}
@Override
public void unloadAll() {
}
@Override
public boolean isTrue(int x, int y, int z, World world) {
return false;
}
@Override
public boolean isTrue(Block block) {
return false;
}
@Override
public void setTrue(int x, int y, int z, World world) {
}
@Override
public void setTrue(Block block) {
}
@Override
public void setFalse(int x, int y, int z, World world) {
}
@Override
public void setFalse(Block block) {
}
@Override
public void cleanUp() {
}
}

View File

@ -1,48 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
public class PrimitiveChunkletStore implements ChunkletStore {
private static final long serialVersionUID = -3453078050608607478L;
/** X, Z, Y */
public boolean[][][] store = new boolean[16][16][64];
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
store[x][z][y] = true;
}
@Override
public void setFalse(int x, int y, int z) {
store[x][z][y] = false;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 64; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
@Override
public void copyFrom(ChunkletStore otherStore) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 64; y++) {
store[x][z][y] = otherStore.isTrue(x, y, z);
}
}
}
}
}

View File

@ -1,180 +0,0 @@
package com.gmail.nossr50.util.blockmeta;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class PrimitiveExChunkletStore implements ChunkletStore, Externalizable {
private static final long serialVersionUID = 8603603827094383873L;
/** X, Z, Y */
public boolean[][][] store = new boolean[16][16][64];
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
store[x][z][y] = true;
}
@Override
public void setFalse(int x, int y, int z) {
store[x][z][y] = false;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 64; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
@Override
public void copyFrom(ChunkletStore otherStore) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 64; y++) {
store[x][z][y] = otherStore.isTrue(x, y, z);
}
}
}
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
byte[] buffer = new byte[2304]; // 2304 is 16*16*9
int bufferIndex = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 64; y++) {
if (store[x][z][y]) {
byte[] temp = constructColumn(x, z);
for (int i = 0; i < 9; i++) {
buffer[bufferIndex] = temp[i];
bufferIndex++;
}
break;
}
}
}
}
out.write(buffer, 0, bufferIndex);
out.flush();
}
// For this we assume that store has been initialized to be all false by now
@Override
public void readExternal(ObjectInput in) throws IOException {
byte[] temp = new byte[9];
// Could probably reorganize this loop to print nasty things if it does not equal 9 or -1
while (in.read(temp, 0, 9) == 9) {
int x = addressByteX(temp[0]);
int z = addressByteZ(temp[0]);
boolean[] yColumn = new boolean[64];
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
yColumn[j + (i * 8)] = (temp[i + 1] & (1 << j)) != 0;
}
}
store[x][z] = yColumn;
}
}
/*
* The column: An array of 9 bytes which represent all y values for a given (x,z) Chunklet-coordinate
*
* The first byte is an address byte, this provides the x and z values.
* The next 8 bytes are all y values from 0 to 63, with each byte containing 8 bits of true/false data
*
* Each of these 8 bytes address to a y value from right to left
*
* Examples:
* 00000001 represents that the lowest y value in this byte is true, all others are off
* 10000000 represents that the highest y value in this byte is true, all others are off
* 10000001 represents that the lowest and highest y values in this byte are true, all others are off
*
* Full columns:
* See comment on Address byte for information on how to use that byte
*
* Example:
* ADDRESS_BYTE 10000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000
* - x, z from ADDRESS_BYTE
* - The next byte contains data from 0 to 7
* - 1 is set in the highest bit position, this is 7 in y coordinate
* - The next byte contains data from 8 to 15
* - 1 is set in the lowest bit position, this is 8 in the y coordinate
* Therefore, for this column: There are true values at (x, 7, z) and (x, 8, z)
*/
private byte[] constructColumn(int x, int z) {
byte[] column = new byte[9];
int index = 1;
column[0] = makeAddressByte(x, z);
for (int i = 0; i < 8; i++) {
byte yCompressed = 0x0;
int subColumnIndex = 8 * i;
int subColumnEnd = subColumnIndex + 8;
for (int y = subColumnIndex; y < subColumnEnd; y++) {
if (store[x][z][y]) {
yCompressed |= 1 << (y % 8);
}
}
column[index] = yCompressed;
index++;
}
return column;
}
/*
* The address byte: A single byte which contains x and z values which correspond to the x and z Chunklet-coordinates
*
* In Chunklet-coordinates, the only valid values are 0-15, so we can fit both into a single byte.
*
* The top 4 bits of the address byte are for the x value
* The bottom 4 bits of the address byte are for the z value
*
* Examples:
* An address byte with a value 00000001 would be split like so:
* - x = 0000 = 0
* - z = 0001 = 1
* => Chunklet coordinates (0, 1)
*
* 01011111
* - x = 0101 = 5
* - z = 1111 = 15
* => Chunklet coordinates (5, 15)
*/
protected static byte makeAddressByte(int x, int z) {
return (byte) ((x << 4) + z);
}
protected static int addressByteX(byte address) {
return (address & 0xF0) >>> 4;
}
protected static int addressByteZ(byte address) {
return address & 0x0F;
}
}

View File

@ -1,10 +0,0 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta;
import org.bukkit.World;
public class ChunkStoreFactory {
protected static ChunkStore getChunkStore(World world, int x, int z) {
// TODO: Add in loading from config what type of store we want.
return new PrimitiveChunkStore(world, x, z);
}
}

View File

@ -1,447 +0,0 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.blockmeta.conversion.BlockStoreConversionZDirectory;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Entity;
import java.io.*;
import java.util.*;
public class HashChunkManager implements ChunkManager {
private final HashMap<UUID, HashMap<Long, McMMOSimpleRegionFile>> regionFiles = new HashMap<>();
public HashMap<String, ChunkStore> store = new HashMap<>();
public ArrayList<BlockStoreConversionZDirectory> converters = new ArrayList<>();
private final HashMap<UUID, Boolean> oldData = new HashMap<>();
@Override
public synchronized void closeAll() {
for (UUID uid : regionFiles.keySet()) {
HashMap<Long, McMMOSimpleRegionFile> worldRegions = regionFiles.get(uid);
for (Iterator<McMMOSimpleRegionFile> worldRegionIterator = worldRegions.values().iterator(); worldRegionIterator.hasNext(); ) {
McMMOSimpleRegionFile rf = worldRegionIterator.next();
if (rf != null) {
rf.close();
worldRegionIterator.remove();
}
}
}
regionFiles.clear();
}
@Override
public synchronized ChunkStore readChunkStore(World world, int x, int z) throws IOException {
McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
InputStream in = rf.getInputStream(x, z);
if (in == null) {
return null;
}
try (ObjectInputStream objectStream = new ObjectInputStream(in)) {
Object o = objectStream.readObject();
if (o instanceof ChunkStore) {
return (ChunkStore) o;
}
throw new RuntimeException("Wrong class type read for chunk meta data for " + x + ", " + z);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
// Assume the format changed
return null;
//throw new RuntimeException("Unable to process chunk meta data for " + x + ", " + z, e);
}
}
@Override
public synchronized void writeChunkStore(World world, int x, int z, ChunkStore data) {
if (!data.isDirty()) {
return;
}
try {
McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
ObjectOutputStream objectStream = new ObjectOutputStream(rf.getOutputStream(x, z));
objectStream.writeObject(data);
objectStream.flush();
objectStream.close();
data.setDirty(false);
}
catch (IOException e) {
throw new RuntimeException("Unable to write chunk meta data for " + x + ", " + z, e);
}
}
@Override
public synchronized void closeChunkStore(World world, int x, int z) {
McMMOSimpleRegionFile rf = getSimpleRegionFile(world, x, z);
if (rf != null) {
rf.close();
}
}
private synchronized McMMOSimpleRegionFile getSimpleRegionFile(World world, int x, int z) {
File directory = new File(world.getWorldFolder(), "mcmmo_regions");
directory.mkdirs();
UUID key = world.getUID();
HashMap<Long, McMMOSimpleRegionFile> worldRegions = regionFiles.computeIfAbsent(key, k -> new HashMap<>());
int rx = x >> 5;
int rz = z >> 5;
long key2 = (((long) rx) << 32) | ((rz) & 0xFFFFFFFFL);
McMMOSimpleRegionFile regionFile = worldRegions.get(key2);
if (regionFile == null) {
File file = new File(directory, "mcmmo_" + rx + "_" + rz + "_.mcm");
regionFile = new McMMOSimpleRegionFile(file, rx, rz);
worldRegions.put(key2, regionFile);
}
return regionFile;
}
@Override
public synchronized void loadChunklet(int cx, int cy, int cz, World world) {
loadChunk(cx, cz, world, null);
}
@Override
public synchronized void unloadChunklet(int cx, int cy, int cz, World world) {
unloadChunk(cx, cz, world);
}
@Override
public synchronized void loadChunk(int cx, int cz, World world, Entity[] entities) {
if (world == null || store.containsKey(world.getName() + "," + cx + "," + cz)) {
return;
}
UUID key = world.getUID();
if (!oldData.containsKey(key)) {
oldData.put(key, (new File(world.getWorldFolder(), "mcmmo_data")).exists());
}
else if (oldData.get(key)) {
if (convertChunk(new File(world.getWorldFolder(), "mcmmo_data"), cx, cz, world, true)) {
return;
}
}
ChunkStore chunkStore = null;
try {
chunkStore = readChunkStore(world, cx, cz);
}
catch (Exception e) { e.printStackTrace(); }
if (chunkStore == null) {
return;
}
store.put(world.getName() + "," + cx + "," + cz, chunkStore);
}
@Override
public synchronized void unloadChunk(int cx, int cz, World world) {
saveChunk(cx, cz, world);
if (store.containsKey(world.getName() + "," + cx + "," + cz)) {
store.remove(world.getName() + "," + cx + "," + cz);
//closeChunkStore(world, cx, cz);
}
}
@Override
public synchronized void saveChunk(int cx, int cz, World world) {
if (world == null) {
return;
}
String key = world.getName() + "," + cx + "," + cz;
if (store.containsKey(key)) {
ChunkStore out = store.get(world.getName() + "," + cx + "," + cz);
if (!out.isDirty()) {
return;
}
writeChunkStore(world, cx, cz, out);
}
}
@Override
public synchronized boolean isChunkLoaded(int cx, int cz, World world) {
if (world == null) {
return false;
}
return store.containsKey(world.getName() + "," + cx + "," + cz);
}
@Override
public synchronized void chunkLoaded(int cx, int cz, World world) {}
@Override
public synchronized void chunkUnloaded(int cx, int cz, World world) {
if (world == null) {
return;
}
unloadChunk(cx, cz, world);
}
@Override
public synchronized void saveWorld(World world) {
if (world == null) {
return;
}
closeAll();
String worldName = world.getName();
List<String> keys = new ArrayList<>(store.keySet());
for (String key : keys) {
String[] info = key.split(",");
if (worldName.equals(info[0])) {
try {
saveChunk(Integer.parseInt(info[1]), Integer.parseInt(info[2]), world);
}
catch (Exception e) {
// Ignore
}
}
}
}
@Override
public synchronized void unloadWorld(World world) {
if (world == null) {
return;
}
String worldName = world.getName();
List<String> keys = new ArrayList<>(store.keySet());
for (String key : keys) {
String[] info = key.split(",");
if (worldName.equals(info[0])) {
try {
unloadChunk(Integer.parseInt(info[1]), Integer.parseInt(info[2]), world);
}
catch (Exception e) {
// Ignore
}
}
}
closeAll();
}
@Override
public synchronized void loadWorld(World world) {}
@Override
public synchronized void saveAll() {
closeAll();
for (World world : mcMMO.p.getServer().getWorlds()) {
saveWorld(world);
}
}
@Override
public synchronized void unloadAll() {
closeAll();
for (World world : mcMMO.p.getServer().getWorlds()) {
unloadWorld(world);
}
}
@Override
public synchronized boolean isTrue(int x, int y, int z, World world) {
if (world == null) {
return false;
}
int cx = x >> 4;
int cz = z >> 4;
String key = world.getName() + "," + cx + "," + cz;
if (!store.containsKey(key)) {
loadChunk(cx, cz, world, null);
}
if (!store.containsKey(key)) {
return false;
}
ChunkStore check = store.get(key);
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
return check.isTrue(ix, y, iz);
}
@Override
public synchronized boolean isTrue(Block block) {
if (block == null) {
return false;
}
return isTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public synchronized boolean isTrue(BlockState blockState) {
if (blockState == null) {
return false;
}
return isTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
@Override
public synchronized void setTrue(int x, int y, int z, World world) {
if (world == null) {
return;
}
int cx = x >> 4;
int cz = z >> 4;
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
String key = world.getName() + "," + cx + "," + cz;
if (!store.containsKey(key)) {
loadChunk(cx, cz, world, null);
}
ChunkStore cStore = store.get(key);
if (cStore == null) {
cStore = ChunkStoreFactory.getChunkStore(world, cx, cz);
store.put(key, cStore);
}
cStore.setTrue(ix, y, iz);
}
@Override
public synchronized void setTrue(Block block) {
if (block == null) {
return;
}
setTrue(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public void setTrue(BlockState blockState) {
if (blockState == null) {
return;
}
setTrue(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
@Override
public synchronized void setFalse(int x, int y, int z, World world) {
if (world == null) {
return;
}
int cx = x >> 4;
int cz = z >> 4;
int ix = Math.abs(x) % 16;
int iz = Math.abs(z) % 16;
String key = world.getName() + "," + cx + "," + cz;
if (!store.containsKey(key)) {
loadChunk(cx, cz, world, null);
}
ChunkStore cStore = store.get(key);
if (cStore == null) {
return; // No need to make a store for something we will be setting to false
}
cStore.setFalse(ix, y, iz);
}
@Override
public synchronized void setFalse(Block block) {
if (block == null) {
return;
}
setFalse(block.getX(), block.getY(), block.getZ(), block.getWorld());
}
@Override
public synchronized void setFalse(BlockState blockState) {
if (blockState == null) {
return;
}
setFalse(blockState.getX(), blockState.getY(), blockState.getZ(), blockState.getWorld());
}
@Override
public synchronized void cleanUp() {}
public synchronized void convertChunk(File dataDir, int cx, int cz, World world) {
convertChunk(dataDir, cx, cz, world, false);
}
public synchronized boolean convertChunk(File dataDir, int cx, int cz, World world, boolean actually) {
if (!actually || !dataDir.exists()) {
return false;
}
File cxDir = new File(dataDir, "" + cx);
if (!cxDir.exists()) {
return false;
}
File czDir = new File(cxDir, "" + cz);
if (!czDir.exists()) {
return false;
}
boolean conversionSet = false;
for (BlockStoreConversionZDirectory converter : this.converters) {
if (converter == null) {
continue;
}
if (converter.taskID >= 0) {
continue;
}
converter.start(world, cxDir, czDir);
conversionSet = true;
break;
}
if (!conversionSet) {
BlockStoreConversionZDirectory converter = new BlockStoreConversionZDirectory();
converter.start(world, cxDir, czDir);
converters.add(converter);
}
return true;
}
}

View File

@ -1,39 +0,0 @@
/*
* This file is part of SpoutPlugin.
*
* Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
* SpoutPlugin is licensed under the GNU Lesser General Public License.
*
* SpoutPlugin is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SpoutPlugin is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.gmail.nossr50.util.blockmeta.chunkmeta;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class McMMOSimpleChunkBuffer extends ByteArrayOutputStream {
final McMMOSimpleRegionFile rf;
final int index;
McMMOSimpleChunkBuffer(McMMOSimpleRegionFile rf, int index) {
super(1024);
this.rf = rf;
this.index = index;
}
@Override
public void close() throws IOException {
rf.write(index, buf, count);
}
}

View File

@ -1,306 +0,0 @@
/*
* This file is part of SpoutPlugin.
*
* Copyright (c) 2011-2012, SpoutDev <http://www.spout.org/>
* SpoutPlugin is licensed under the GNU Lesser General Public License.
*
* SpoutPlugin is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SpoutPlugin is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.gmail.nossr50.util.blockmeta.chunkmeta;
import java.io.*;
import java.util.ArrayList;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
public class McMMOSimpleRegionFile {
private RandomAccessFile file;
private final int[] dataStart = new int[1024];
private final int[] dataActualLength = new int[1024];
private final int[] dataLength = new int[1024];
private final ArrayList<Boolean> inuse = new ArrayList<>();
private int segmentSize;
private int segmentMask;
private final int rx;
private final int rz;
private final int defaultSegmentSize;
private final File parent;
@SuppressWarnings("unused")
private long lastAccessTime = System.currentTimeMillis();
@SuppressWarnings("unused")
private static final long TIMEOUT_TIME = 300000; // 5 min
public McMMOSimpleRegionFile(File f, int rx, int rz) {
this(f, rx, rz, 10);
}
public McMMOSimpleRegionFile(File f, int rx, int rz, int defaultSegmentSize) {
this.rx = rx;
this.rz = rz;
this.defaultSegmentSize = defaultSegmentSize;
this.parent = f;
lastAccessTime = System.currentTimeMillis();
if (file == null) {
try {
this.file = new RandomAccessFile(parent, "rw");
if (file.length() < 4096 * 3) {
for (int i = 0; i < 1024 * 3; i++) {
file.writeInt(0);
}
file.seek(4096 * 2);
file.writeInt(defaultSegmentSize);
}
file.seek(4096 * 2);
this.segmentSize = file.readInt();
this.segmentMask = (1 << segmentSize) - 1;
int reservedSegments = this.sizeToSegments(4096 * 3);
for (int i = 0; i < reservedSegments; i++) {
while (inuse.size() <= i) {
inuse.add(false);
}
inuse.set(i, true);
}
file.seek(0);
for (int i = 0; i < 1024; i++) {
dataStart[i] = file.readInt();
}
for (int i = 0; i < 1024; i++) {
dataActualLength[i] = file.readInt();
dataLength[i] = sizeToSegments(dataActualLength[i]);
setInUse(i, true);
}
extendFile();
}
catch (IOException fnfe) {
throw new RuntimeException(fnfe);
}
}
}
public synchronized final RandomAccessFile getFile() {
lastAccessTime = System.currentTimeMillis();
if (file == null) {
try {
this.file = new RandomAccessFile(parent, "rw");
if (file.length() < 4096 * 3) {
for (int i = 0; i < 1024 * 3; i++) {
file.writeInt(0);
}
file.seek(4096 * 2);
file.writeInt(defaultSegmentSize);
}
file.seek(4096 * 2);
this.segmentSize = file.readInt();
this.segmentMask = (1 << segmentSize) - 1;
int reservedSegments = this.sizeToSegments(4096 * 3);
for (int i = 0; i < reservedSegments; i++) {
while (inuse.size() <= i) {
inuse.add(false);
}
inuse.set(i, true);
}
file.seek(0);
for (int i = 0; i < 1024; i++) {
dataStart[i] = file.readInt();
}
for (int i = 0; i < 1024; i++) {
dataActualLength[i] = file.readInt();
dataLength[i] = sizeToSegments(dataActualLength[i]);
setInUse(i, true);
}
extendFile();
}
catch (IOException fnfe) {
throw new RuntimeException(fnfe);
}
}
return file;
}
public synchronized boolean testCloseTimeout() {
/*
if (System.currentTimeMillis() - TIMEOUT_TIME > lastAccessTime) {
close();
return true;
}
*/
return false;
}
public synchronized DataOutputStream getOutputStream(int x, int z) {
int index = getChunkIndex(x, z);
return new DataOutputStream(new DeflaterOutputStream(new McMMOSimpleChunkBuffer(this, index)));
}
public synchronized DataInputStream getInputStream(int x, int z) throws IOException {
int index = getChunkIndex(x, z);
int actualLength = dataActualLength[index];
if (actualLength == 0) {
return null;
}
byte[] data = new byte[actualLength];
getFile().seek(dataStart[index] << segmentSize);
getFile().readFully(data);
return new DataInputStream(new InflaterInputStream(new ByteArrayInputStream(data)));
}
synchronized void write(int index, byte[] buffer, int size) throws IOException {
int oldStart = setInUse(index, false);
int start = findSpace(oldStart, size);
getFile().seek(start << segmentSize);
getFile().write(buffer, 0, size);
dataStart[index] = start;
dataActualLength[index] = size;
dataLength[index] = sizeToSegments(size);
setInUse(index, true);
saveFAT();
}
public synchronized void close() {
try {
if (file != null) {
file.seek(4096 * 2);
file.close();
}
file = null;
}
catch (IOException ioe) {
throw new RuntimeException("Unable to close file", ioe);
}
}
private synchronized int setInUse(int index, boolean used) {
if (dataActualLength[index] == 0) {
return dataStart[index];
}
int start = dataStart[index];
int end = start + dataLength[index];
for (int i = start; i < end; i++) {
while (i > inuse.size() - 1) {
inuse.add(false);
}
Boolean old = inuse.set(i, used);
if (old != null && old == used) {
if (old) {
throw new IllegalStateException("Attempting to overwrite an in-use segment");
}
throw new IllegalStateException("Attempting to delete empty segment");
}
}
return dataStart[index];
}
private synchronized void extendFile() throws IOException {
long extend = (-getFile().length()) & segmentMask;
getFile().seek(getFile().length());
while ((extend--) > 0) {
getFile().write(0);
}
}
private synchronized int findSpace(int oldStart, int size) {
int segments = sizeToSegments(size);
boolean oldFree = true;
for (int i = oldStart; i < inuse.size() && i < oldStart + segments; i++) {
if (inuse.get(i)) {
oldFree = false;
break;
}
}
if (oldFree) {
return oldStart;
}
int start = 0;
int end = 0;
while (end < inuse.size()) {
if (inuse.get(end)) {
end++;
start = end;
}
else {
end++;
}
if (end - start >= segments) {
return start;
}
}
return start;
}
private synchronized int sizeToSegments(int size) {
if (size <= 0) {
return 1;
}
return ((size - 1) >> segmentSize) + 1;
}
private synchronized Integer getChunkIndex(int x, int z) {
if (rx != (x >> 5) || rz != (z >> 5)) {
throw new RuntimeException(x + ", " + z + " not in region " + rx + ", " + rz);
}
x = x & 0x1F;
z = z & 0x1F;
return (x << 5) + z;
}
private synchronized void saveFAT() throws IOException {
getFile().seek(0);
for (int i = 0; i < 1024; i++) {
getFile().writeInt(dataStart[i]);
}
for (int i = 0; i < 1024; i++) {
getFile().writeInt(dataActualLength[i]);
}
}
}

View File

@ -1,147 +0,0 @@
package com.gmail.nossr50.util.blockmeta.chunkmeta;
import com.gmail.nossr50.util.blockmeta.ChunkletStore;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.UUID;
public class PrimitiveChunkStore implements ChunkStore {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
/** X, Z, Y */
public boolean[][][] store;
private static final int CURRENT_VERSION = 7;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private int cx;
private int cz;
private UUID worldUid;
public PrimitiveChunkStore(World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.store = new boolean[16][16][world.getMaxHeight()];
}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = true;
dirty = true;
}
@Override
public void setFalse(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = false;
dirty = true;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
@Override
public void copyFrom(ChunkletStore otherStore) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
store[x][z][y] = otherStore.isTrue(x, y, z);
}
}
}
dirty = true;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeObject(store);
dirty = false;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
int magic = in.readInt();
// Can be used to determine the format of the file
int fileVersionNumber = in.readInt();
if (magic != MAGIC_NUMBER) {
fileVersionNumber = 0;
}
long lsb = in.readLong();
long msb = in.readLong();
worldUid = new UUID(msb, lsb);
cx = in.readInt();
cz = in.readInt();
store = (boolean[][][]) in.readObject();
if (fileVersionNumber < 5) {
fixArray();
dirty = true;
}
}
private void fixArray() {
boolean[][][] temp = this.store;
this.store = new boolean[16][16][Bukkit.getWorld(worldUid).getMaxHeight()];
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
try {
store[x][z][y] = temp[x][y][z];
}
catch (Exception e) { e.printStackTrace(); }
}
}
}
}
}

View File

@ -1,90 +0,0 @@
package com.gmail.nossr50.util.blockmeta.conversion;
import com.gmail.nossr50.config.HiddenConfig;
import com.gmail.nossr50.mcMMO;
import org.bukkit.scheduler.BukkitScheduler;
import java.io.File;
public class BlockStoreConversionMain implements Runnable {
private int taskID, i;
private org.bukkit.World world;
BukkitScheduler scheduler;
File dataDir;
File[] xDirs;
BlockStoreConversionXDirectory[] converters;
public BlockStoreConversionMain(org.bukkit.World world) {
this.taskID = -1;
this.world = world;
this.scheduler = mcMMO.p.getServer().getScheduler();
this.dataDir = new File(this.world.getWorldFolder(), "mcmmo_data");
this.converters = new BlockStoreConversionXDirectory[HiddenConfig.getInstance().getConversionRate()];
}
public void start() {
if (this.taskID >= 0) {
return;
}
this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
}
@Override
public void run() {
if (!this.dataDir.exists()) {
softStop();
return;
}
if (!this.dataDir.isDirectory()) {
this.dataDir.delete();
softStop();
return;
}
if (this.dataDir.listFiles().length <= 0) {
this.dataDir.delete();
softStop();
return;
}
this.xDirs = this.dataDir.listFiles();
for (this.i = 0; (this.i < HiddenConfig.getInstance().getConversionRate()) && (this.i < this.xDirs.length); this.i++) {
if (this.converters[this.i] == null) {
this.converters[this.i] = new BlockStoreConversionXDirectory();
}
this.converters[this.i].start(this.world, this.xDirs[this.i]);
}
softStop();
}
public void stop() {
if (this.taskID < 0) {
return;
}
this.scheduler.cancelTask(this.taskID);
this.taskID = -1;
}
public void softStop() {
stop();
if (this.dataDir.exists() && this.dataDir.isDirectory()) {
start();
return;
}
mcMMO.p.getLogger().info("Finished converting the storage for " + world.getName() + ".");
this.dataDir = null;
this.xDirs = null;
this.world = null;
this.scheduler = null;
this.converters = null;
}
}

View File

@ -1,80 +0,0 @@
package com.gmail.nossr50.util.blockmeta.conversion;
import com.gmail.nossr50.config.HiddenConfig;
import com.gmail.nossr50.mcMMO;
import org.bukkit.scheduler.BukkitScheduler;
import java.io.File;
public class BlockStoreConversionXDirectory implements Runnable {
private int taskID, i;
private org.bukkit.World world;
BukkitScheduler scheduler;
File dataDir;
File[] zDirs;
BlockStoreConversionZDirectory[] converters;
public BlockStoreConversionXDirectory() {
this.taskID = -1;
}
public void start(org.bukkit.World world, File dataDir) {
this.world = world;
this.scheduler = mcMMO.p.getServer().getScheduler();
this.converters = new BlockStoreConversionZDirectory[HiddenConfig.getInstance().getConversionRate()];
this.dataDir = dataDir;
if (this.taskID >= 0) {
return;
}
this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
}
@Override
public void run() {
if (!this.dataDir.exists()) {
stop();
return;
}
if (!this.dataDir.isDirectory()) {
this.dataDir.delete();
stop();
return;
}
if (this.dataDir.listFiles().length <= 0) {
this.dataDir.delete();
stop();
return;
}
this.zDirs = this.dataDir.listFiles();
for (this.i = 0; (this.i < HiddenConfig.getInstance().getConversionRate()) && (this.i < this.zDirs.length); this.i++) {
if (this.converters[this.i] == null) {
this.converters[this.i] = new BlockStoreConversionZDirectory();
}
this.converters[this.i].start(this.world, this.dataDir, this.zDirs[this.i]);
}
stop();
}
public void stop() {
if (this.taskID < 0) {
return;
}
this.scheduler.cancelTask(this.taskID);
this.taskID = -1;
this.dataDir = null;
this.zDirs = null;
this.world = null;
this.scheduler = null;
this.converters = null;
}
}

View File

@ -1,191 +0,0 @@
package com.gmail.nossr50.util.blockmeta.conversion;
import com.gmail.nossr50.mcMMO;
import com.gmail.nossr50.util.blockmeta.ChunkletStore;
import com.gmail.nossr50.util.blockmeta.HashChunkletManager;
import com.gmail.nossr50.util.blockmeta.PrimitiveChunkletStore;
import com.gmail.nossr50.util.blockmeta.PrimitiveExChunkletStore;
import com.gmail.nossr50.util.blockmeta.chunkmeta.HashChunkManager;
import com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore;
import org.bukkit.scheduler.BukkitScheduler;
import java.io.File;
public class BlockStoreConversionZDirectory implements Runnable {
public int taskID, cx, cz, x, y, z, y2, xPos, zPos, cxPos, czPos;
private String cxs, czs, chunkletName, chunkName;
private org.bukkit.World world;
private BukkitScheduler scheduler;
private File xDir, dataDir;
private HashChunkletManager manager;
private HashChunkManager newManager;
private ChunkletStore tempChunklet;
private PrimitiveChunkletStore primitiveChunklet = null;
private PrimitiveExChunkletStore primitiveExChunklet = null;
private PrimitiveChunkStore currentChunk;
private boolean[] oldArray, newArray;
public BlockStoreConversionZDirectory() {
this.taskID = -1;
}
public void start(org.bukkit.World world, File xDir, File dataDir) {
this.world = world;
this.scheduler = mcMMO.p.getServer().getScheduler();
this.manager = new HashChunkletManager();
this.newManager = (HashChunkManager) mcMMO.getPlaceStore();
this.dataDir = dataDir;
this.xDir = xDir;
if (this.taskID >= 0) {
return;
}
this.taskID = this.scheduler.runTaskLater(mcMMO.p, this, 1).getTaskId();
}
@Override
public void run() {
if (!this.dataDir.exists()) {
stop();
return;
}
if (!this.dataDir.isDirectory()) {
this.dataDir.delete();
stop();
return;
}
if (this.dataDir.listFiles().length <= 0) {
this.dataDir.delete();
stop();
return;
}
this.cxs = this.xDir.getName();
this.czs = this.dataDir.getName();
this.cx = 0;
this.cz = 0;
try {
this.cx = Integer.parseInt(this.cxs);
this.cz = Integer.parseInt(this.czs);
}
catch (Exception e) {
this.dataDir.delete();
stop();
return;
}
this.manager.loadChunk(this.cx, this.cz, this.world);
for (this.y = 0; this.y < (this.world.getMaxHeight() / 64); this.y++) {
this.chunkletName = this.world.getName() + "," + this.cx + "," + this.cz + "," + this.y;
this.tempChunklet = this.manager.store.get(this.chunkletName);
if (this.tempChunklet instanceof PrimitiveChunkletStore) {
this.primitiveChunklet = (PrimitiveChunkletStore) this.tempChunklet;
}
else if (this.tempChunklet instanceof PrimitiveExChunkletStore) {
this.primitiveExChunklet = (PrimitiveExChunkletStore) this.tempChunklet;
}
if (this.tempChunklet == null) {
continue;
}
this.chunkName = this.world.getName() + "," + this.cx + "," + this.cz;
this.currentChunk = (PrimitiveChunkStore) this.newManager.store.get(this.chunkName);
if (this.currentChunk != null) {
this.xPos = this.cx * 16;
this.zPos = this.cz * 16;
for (this.x = 0; this.x < 16; this.x++) {
for (this.z = 0; this.z < 16; this.z++) {
this.cxPos = this.xPos + this.x;
this.czPos = this.zPos + this.z;
for (this.y2 = (64 * this.y); this.y2 < (64 * this.y + 64); this.y2++) {
try {
if (!this.manager.isTrue(this.cxPos, this.y2, this.czPos, this.world)) {
continue;
}
this.newManager.setTrue(this.cxPos, this.y2, this.czPos, this.world);
}
catch (Exception e) { e.printStackTrace(); }
}
}
}
continue;
}
this.newManager.setTrue(this.cx * 16, 0, this.cz * 16, this.world);
this.newManager.setFalse(this.cx * 16, 0, this.cz * 16, this.world);
this.currentChunk = (PrimitiveChunkStore) this.newManager.store.get(this.chunkName);
for (this.x = 0; this.x < 16; this.x++) {
for (this.z = 0; this.z < 16; this.z++) {
if (this.primitiveChunklet != null) {
this.oldArray = this.primitiveChunklet.store[x][z];
}
if (this.primitiveExChunklet != null) {
this.oldArray = this.primitiveExChunklet.store[x][z];
}
else {
return;
}
this.newArray = this.currentChunk.store[x][z];
if (this.oldArray.length < 64) {
return;
}
else if (this.newArray.length < ((this.y * 64) + 64)) {
return;
}
System.arraycopy(this.oldArray, 0, this.newArray, (this.y * 64), 64);
}
}
}
this.manager.unloadChunk(this.cx, this.cz, this.world);
this.newManager.unloadChunk(this.cx, this.cz, this.world);
for (File yFile : dataDir.listFiles()) {
if (!yFile.exists()) {
continue;
}
yFile.delete();
}
stop();
}
public void stop() {
if (this.taskID < 0) {
return;
}
this.scheduler.cancelTask(taskID);
this.taskID = -1;
this.cxs = null;
this.czs = null;
this.chunkletName = null;
this.chunkName = null;
this.manager = null;
this.xDir = null;
this.dataDir = null;
this.tempChunklet = null;
this.primitiveChunklet = null;
this.primitiveExChunklet = null;
this.currentChunk = null;
}
}

View File

@ -0,0 +1,308 @@
import com.gmail.nossr50.util.blockmeta.*;
import com.google.common.io.Files;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.*;
import java.util.UUID;
import static org.mockito.Mockito.mock;
/**
* Could be alot 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)
@PrepareForTest(Bukkit.class)
public class ChunkStoreTest {
private static File tempDir;
@BeforeClass
public static void setUpClass() {
tempDir = Files.createTempDir();
}
@AfterClass
public static void tearDownClass() {
recursiveDelete(tempDir);
}
private World mockWorld;
@Before
public void setUpMock(){
UUID worldUUID = UUID.randomUUID();
mockWorld = mock(World.class);
Mockito.when(mockWorld.getUID()).thenReturn(worldUUID);
Mockito.when(mockWorld.getMaxHeight()).thenReturn(256);
Mockito.when(mockWorld.getWorldFolder()).thenReturn(tempDir);
PowerMockito.mockStatic(Bukkit.class);
Mockito.when(Bukkit.getWorld(worldUUID)).thenReturn(mockWorld);
}
@Test
public void testSetValue() {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
original.setTrue(0, 0, 0);
Assert.assertTrue(original.isTrue(0, 0, 0));
original.setFalse(0, 0, 0);
Assert.assertFalse(original.isTrue(0, 0, 0));
}
@Test
public void testIsEmpty() {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 0, 0);
Assert.assertTrue(original.isEmpty());
original.setTrue(0, 0, 0);
original.setFalse(0, 0, 0);
Assert.assertTrue(original.isEmpty());
}
@Test
public void testRoundTrip() throws IOException {
BitSetChunkStore original = new BitSetChunkStore(mockWorld, 1, 2);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
public void testChunkCoords() throws IOException {
for (int x = -96; x < 0; x++) {
int cx = x >> 4;
int ix = Math.abs(x) % 16;
System.out.print(cx + ":" + ix + " ");
}
}
@Test
public void testUpgrade() throws IOException {
LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 32);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
byte[] serializedBytes = serializeChunkstore(original);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(new DataInputStream(new ByteArrayInputStream(serializedBytes)));
assertEqual(original, deserialized);
}
@Test
public void testSimpleRegionRoundtrip() throws IOException {
LegacyChunkStore original = new LegacyChunkStore(mockWorld, 12, 12);
original.setTrue(14, 89, 12);
original.setTrue(14, 90, 12);
original.setTrue(13, 89, 12);
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
try (DataOutputStream outputStream = region.getOutputStream(12, 12)){
outputStream.write(serializeChunkstore(original));
}
region.close();
region = new McMMOSimpleRegionFile(file, 0, 0);
try (DataInputStream is = region.getInputStream(original.getChunkX(), original.getChunkZ()))
{
Assert.assertNotNull(is);
ChunkStore deserialized = BitSetChunkStore.Serialization.readChunkStore(is);
assertEqual(original, deserialized);
}
region.close();
file.delete();
}
@Test
public void testSimpleRegionRejectsOutOfBounds() {
File file = new File(tempDir, "SimpleRegionRoundTrip.region");
McMMOSimpleRegionFile region = new McMMOSimpleRegionFile(file, 0, 0);
assertThrows(() -> region.getOutputStream(-1, 0), IndexOutOfBoundsException.class);
assertThrows(() -> region.getOutputStream(0, -1), IndexOutOfBoundsException.class);
assertThrows(() -> region.getOutputStream(32, 0), IndexOutOfBoundsException.class);
assertThrows(() -> region.getOutputStream(0, 32), IndexOutOfBoundsException.class);
region.close();
}
@Test
public void testChunkStoreRejectsOutOfBounds() {
ChunkStore chunkStore = new BitSetChunkStore(mockWorld, 0, 0);
assertThrows(() -> chunkStore.setTrue(-1, 0, 0), IndexOutOfBoundsException.class);
assertThrows(() -> chunkStore.setTrue(0, -1, 0), IndexOutOfBoundsException.class);
assertThrows(() -> chunkStore.setTrue(0, 0, -1), IndexOutOfBoundsException.class);
assertThrows(() -> chunkStore.setTrue(16, 0, 0), IndexOutOfBoundsException.class);
assertThrows(() -> chunkStore.setTrue(0, mockWorld.getMaxHeight(), 0), IndexOutOfBoundsException.class);
assertThrows(() -> chunkStore.setTrue(0, 0, 16), IndexOutOfBoundsException.class);
}
@Test
public void testRegressionChunkMirrorBug() {
ChunkManager chunkManager = new HashChunkManager();
chunkManager.setTrue(15,0,15, mockWorld);
chunkManager.setFalse(-15, 0, -15, mockWorld);
Assert.assertTrue(chunkManager.isTrue(15, 0, 15, mockWorld));
}
private interface Delegate {
void run();
}
private void assertThrows(Delegate delegate, Class<?> clazz) {
try {
delegate.run();
Assert.fail(); // We didn't throw
}
catch (Throwable t) {
Assert.assertTrue(t.getClass().equals(clazz));
}
}
private void assertEqual(ChunkStore expected, ChunkStore actual)
{
Assert.assertEquals(expected.getChunkX(), actual.getChunkX());
Assert.assertEquals(expected.getChunkZ(), actual.getChunkZ());
Assert.assertEquals(expected.getWorldId(), actual.getWorldId());
for (int y = 0; y < 256; y++)
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++)
Assert.assertTrue(expected.isTrue(x, y, z) == actual.isTrue(x, y, z));
}
private static void recursiveDelete(File directoryToBeDeleted) {
if (directoryToBeDeleted.isDirectory()) {
for (File file : directoryToBeDeleted.listFiles()) {
recursiveDelete(file);
}
}
directoryToBeDeleted.delete();
}
private static byte[] serializeChunkstore(ChunkStore chunkStore) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (chunkStore instanceof BitSetChunkStore)
BitSetChunkStore.Serialization.writeChunkStore(new DataOutputStream(byteArrayOutputStream), chunkStore);
else
new UnitTestObjectOutputStream(byteArrayOutputStream).writeObject(chunkStore); // Serializes the class as if it were the old PrimitiveChunkStore
return byteArrayOutputStream.toByteArray();
}
public static class LegacyChunkStore implements ChunkStore, Serializable {
private static final long serialVersionUID = -1L;
transient private boolean dirty = false;
public boolean[][][] store;
private static final int CURRENT_VERSION = 7;
private static final int MAGIC_NUMBER = 0xEA5EDEBB;
private int cx;
private int cz;
private UUID worldUid;
public LegacyChunkStore(World world, int cx, int cz) {
this.cx = cx;
this.cz = cz;
this.worldUid = world.getUID();
this.store = new boolean[16][16][world.getMaxHeight()];
}
@Override
public boolean isDirty() {
return dirty;
}
@Override
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
@Override
public int getChunkX() {
return cx;
}
@Override
public int getChunkZ() {
return cz;
}
@Override
public UUID getWorldId() {
return worldUid;
}
@Override
public boolean isTrue(int x, int y, int z) {
return store[x][z][y];
}
@Override
public void setTrue(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = true;
dirty = true;
}
@Override
public void setFalse(int x, int y, int z) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = false;
dirty = true;
}
@Override
public void set(int x, int y, int z, boolean value) {
if (y >= store[0][0].length || y < 0)
return;
store[x][z][y] = value;
dirty = true;
}
@Override
public boolean isEmpty() {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < store[0][0].length; y++) {
if (store[x][z][y]) {
return false;
}
}
}
}
return true;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(MAGIC_NUMBER);
out.writeInt(CURRENT_VERSION);
out.writeLong(worldUid.getLeastSignificantBits());
out.writeLong(worldUid.getMostSignificantBits());
out.writeInt(cx);
out.writeInt(cz);
out.writeObject(store);
dirty = false;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new UnsupportedOperationException();
}
}
private static class UnitTestObjectOutputStream extends ObjectOutputStream {
public UnitTestObjectOutputStream(OutputStream outputStream) throws IOException {
super(outputStream);
}
@Override
public void writeUTF(String str) throws IOException {
// Pretend to be the old class
if (str.equals(LegacyChunkStore.class.getName()))
str = "com.gmail.nossr50.util.blockmeta.chunkmeta.PrimitiveChunkStore";
super.writeUTF(str);
}
}
}