Reworked thread safe cache solution.

Also performed refactoring.

And added some threaded tests.
This commit is contained in:
Harrison Deng 2020-04-28 16:53:23 -05:00
parent 7c073cdc6e
commit b30d689183
22 changed files with 1800 additions and 177 deletions

11
.vscode/launch.json vendored
View File

@ -4,6 +4,13 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"type": "java",
"name": "CodeLens (Launch) - UniBiomeIslandGeneratorTest",
"request": "launch",
"mainClass": "ca.recrown.islandsurvivalcraft.world.generation.UniBiomeIslandGeneratorTest",
"projectName": "IslandSurvivalCraft"
},
{ {
"type": "java", "type": "java",
"name": "Debug (Launch) - Current File", "name": "Debug (Launch) - Current File",
@ -16,7 +23,9 @@
"request": "attach", "request": "attach",
"hostName": "localhost", "hostName": "localhost",
"port": 25566, "port": 25566,
"sourcePaths": ["src/main/java"], "sourcePaths": [
"src/main/java"
],
"preLaunchTask": "start test server", "preLaunchTask": "start test server",
"postDebugTask": "stop test server" "postDebugTask": "stop test server"
} }

10
pom.xml
View File

@ -14,6 +14,12 @@
</repository> </repository>
</repositories> </repositories>
<dependencies> <dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
@ -41,6 +47,10 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version> <version>2.22.2</version>
</plugin> </plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins> </plugins>
</build> </build>
<properties> <properties>

View File

@ -11,6 +11,7 @@ public class IslandSurvivalCraft extends JavaPlugin {
@Override @Override
public void onEnable() { public void onEnable() {
super.onEnable(); super.onEnable();
} }

View File

@ -4,7 +4,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map.Entry; import java.util.Map.Entry;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
public class Utilities { public class Utilities {
public final static int CHUNK_SIZE = 16; public final static int CHUNK_SIZE = 16;

View File

@ -1,22 +1,20 @@
package ca.recrown.islandsurvivalcraft.caching; package ca.recrown.islandsurvivalcraft.caching;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
public class Cache<Key, Value> { public class Cache<K, V> {
private final int maxSize; private final int maxSize;
private final ConcurrentHashMap<Key, CacheValue<Value>> data; private final ConcurrentHashMap<K, CacheValue<K, V>> data;
private final ConcurrentLinkedQueue<Key> occurrenceOrder; private final UsageStack<K, V> usage = new UsageStack<>();
private ReentrantReadWriteLock cleaningLock = new ReentrantReadWriteLock(true); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private WriteLock writeLock = cleaningLock.writeLock(); private final WriteLock writeLock = lock.writeLock();
private ReadLock readLock = cleaningLock.readLock(); private final ReadLock readLock = lock.readLock();
public Cache(int maxSize) { public Cache(int maxSize) {
data = new ConcurrentHashMap<>(maxSize + 1, 0.75f, 6); data = new ConcurrentHashMap<>(maxSize + 1, 0.75f, 6);
occurrenceOrder = new ConcurrentLinkedQueue<>();
this.maxSize = maxSize; this.maxSize = maxSize;
} }
@ -24,30 +22,24 @@ public class Cache<Key, Value> {
this(1024); this(1024);
} }
public void setValue(Key key, Value value) { public void set(K key, V value) {
CacheValue<Value> previous = data.get(key);
CacheValue<Value> fresh = new CacheValue<>(value);
if (previous != null) fresh.occurrence += previous.occurrence;
occurrenceOrder.add(key);
data.put(key, fresh);
if (data.size() > maxSize) {
int occ = 0;
do {
Key potentialKey = occurrenceOrder.poll();
CacheValue<Value> potential = data.get(potentialKey);
potential.occurrence--;
writeLock.lock(); writeLock.lock();
try { try {
occ = potential.occurrence; if (data.containsKey(key)) {
if (occ < 1) { data.get(key).value = value;
data.remove(potentialKey); } else {
CacheValue<K, V> val = new CacheValue<>();
val.key = key;
val.value = value;
data.put(key, val);
usage.add(val);
if (data.size() > maxSize) {
data.remove(usage.pop().key);
}
} }
} finally { } finally {
writeLock.unlock(); writeLock.unlock();
} }
} while (occ > 0);
}
} }
/** /**
@ -57,22 +49,26 @@ public class Cache<Key, Value> {
* @param key the key associated with the value. * @param key the key associated with the value.
* @return the value associated to the key. * @return the value associated to the key.
*/ */
public Value getValue(Key key) { public V get(K key) {
CacheValue<Value> res = null;
readLock.lock(); readLock.lock();
CacheValue<K, V> value = null;
try { try {
res = data.get(key); if (!data.containsKey(key)) return null;
if (res == null) return null; value = data.get(key);
res.occurrence++; usage.tryMoveToTop(value);
occurrenceOrder.add(key);
} finally { } finally {
readLock.unlock(); readLock.unlock();
} }
return res.data; return value.value;
} }
public void clearCache() { public void clearCache() {
writeLock.lock();
try {
data.clear(); data.clear();
occurrenceOrder.clear(); usage.clear();
} finally {
writeLock.unlock();
}
} }
} }

View File

@ -1,10 +1,7 @@
package ca.recrown.islandsurvivalcraft.caching; package ca.recrown.islandsurvivalcraft.caching;
class CacheValue<ValueType> { class CacheValue<KeyType, ValueType> {
public volatile int occurrence = 1; public volatile KeyType key;
public final ValueType data; public volatile ValueType value;
public volatile CacheValue<KeyType, ValueType> front, back;
public CacheValue(ValueType value) {
this.data = value;
}
} }

View File

@ -0,0 +1,89 @@
package ca.recrown.islandsurvivalcraft.caching;
import java.util.concurrent.locks.ReentrantLock;
public class UsageStack<K, V> {
private volatile CacheValue<K, V> first, last;
private final ReentrantLock lock = new ReentrantLock();
private void addToTop(CacheValue<K, V> value) {
if (first != null) {
first.front = value;
value.back = first;
} else {
last = value;
}
value.front = null;
first = value;
}
private void removeValue(CacheValue<K, V> value) {
if (value.front != null) {
value.front.back = value.back;
} else {
first = value.back;
}
if (value.back != null) {
value.back.front = value.front;
} else {
last = value.front;
}
value.front = null;
value.back = null;
}
public void moveToTop(CacheValue<K, V> value) {
lock.lock();
try {
removeValue(value);
addToTop(value);
} finally {
lock.unlock();
}
}
public boolean tryMoveToTop(CacheValue<K, V> value) {
boolean success = false;
success = lock.tryLock();
if (success) {
try {
removeValue(value);
addToTop(value);
} finally {
lock.unlock();
}
}
return success;
}
public void add(CacheValue<K, V> value) {
lock.lock();
try {
addToTop(value);
} finally {
lock.unlock();
}
}
public CacheValue<K, V> pop() {
CacheValue<K, V> cacheValue;
lock.lock();
try {
cacheValue = last;
removeValue(last);
} finally {
lock.unlock();
}
return cacheValue;
}
public void clear() {
lock.lock();
try {
first = null;
last = null;
} finally {
lock.unlock();
}
}
}

View File

@ -1,4 +1,4 @@
package ca.recrown.islandsurvivalcraft.types; package ca.recrown.islandsurvivalcraft.datatypes;
import java.util.Objects; import java.util.Objects;

View File

@ -1,6 +1,6 @@
package ca.recrown.islandsurvivalcraft.floodfill; package ca.recrown.islandsurvivalcraft.floodfill;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
public interface Floodable { public interface Floodable {
/** /**

View File

@ -3,7 +3,7 @@ package ca.recrown.islandsurvivalcraft.floodfill;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
public class Flooder { public class Flooder {
private final Point2 start; private final Point2 start;

View File

@ -2,7 +2,7 @@ package ca.recrown.islandsurvivalcraft.pathfinding;
import java.util.Objects; import java.util.Objects;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
public class DFSNode extends Point2 implements Comparable<DFSNode> { public class DFSNode extends Point2 implements Comparable<DFSNode> {
private DFSNode goal; private DFSNode goal;

View File

@ -6,7 +6,7 @@ import java.util.Queue;
import org.apache.commons.lang.NullArgumentException; import org.apache.commons.lang.NullArgumentException;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
public class DepthFirstSearch { public class DepthFirstSearch {
private Queue<DFSNode> queue; private Queue<DFSNode> queue;

View File

@ -4,7 +4,7 @@ import java.util.Random;
import org.bukkit.util.noise.SimplexOctaveGenerator; import org.bukkit.util.noise.SimplexOctaveGenerator;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.caching.Cache; import ca.recrown.islandsurvivalcraft.caching.Cache;
import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateValidatable; import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateValidatable;
import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch; import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch;
@ -24,11 +24,11 @@ public class IslandWorldMapper implements CoordinateValidatable {
private final float shallowPortion = 0.015f; private final float shallowPortion = 0.015f;
private final DepthFirstSearch dfs; private final DepthFirstSearch dfs;
public IslandWorldMapper(Random random) { public IslandWorldMapper(Random random, Cache<Point2, Double> blockValueCache) {
dfs = new DepthFirstSearch(this); dfs = new DepthFirstSearch(this);
this.noiseGenerator = new SimplexOctaveGenerator(random, noiseOctaves); this.noiseGenerator = new SimplexOctaveGenerator(random, noiseOctaves);
noiseGenerator.setScale(scale); noiseGenerator.setScale(scale);
blockValueCache = new Cache<>(65536); this.blockValueCache = blockValueCache;
} }
/** /**
@ -114,7 +114,7 @@ public class IslandWorldMapper implements CoordinateValidatable {
public double getWorldBlockValue(int worldX, int worldZ) { public double getWorldBlockValue(int worldX, int worldZ) {
Point2 p = new Point2(worldX, worldZ); Point2 p = new Point2(worldX, worldZ);
Double res = blockValueCache.getValue(p); Double res = blockValueCache.get(p);
if (res == null) { if (res == null) {
double portionSea = 1f - (this.islandBlockGenerationPercent / 100f); double portionSea = 1f - (this.islandBlockGenerationPercent / 100f);
double shift = 1f - 2 * portionSea; double shift = 1f - 2 * portionSea;
@ -133,7 +133,7 @@ public class IslandWorldMapper implements CoordinateValidatable {
} else { } else {
res = noise / maxPos; res = noise / maxPos;
} }
blockValueCache.setValue(p, res); blockValueCache.set(p, res);
return res; return res;
} }

View File

@ -3,6 +3,8 @@ package ca.recrown.islandsurvivalcraft.world.generation;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import ca.recrown.islandsurvivalcraft.caching.Cache;
import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.world.BiomeSelector; import ca.recrown.islandsurvivalcraft.world.BiomeSelector;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper; import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
@ -13,6 +15,7 @@ public interface BiomeGenerator {
* The array should store the columns of biomes for the entire chunk. * The array should store the columns of biomes for the entire chunk.
* It doesn't need to be populated on the first call as this method will be called once for every column in the chunk. * It doesn't need to be populated on the first call as this method will be called once for every column in the chunk.
* However, if some biomes can be set without repetative calls, doing so will prevent this method from being called for those locals. * However, if some biomes can be set without repetative calls, doing so will prevent this method from being called for those locals.
*
* @param biomesArray The array of biomes that are to be saturated. * @param biomesArray The array of biomes that are to be saturated.
* @param world The current world. * @param world The current world.
* @param chunkX The X coordinate of the chunk. * @param chunkX The X coordinate of the chunk.
@ -21,13 +24,8 @@ public interface BiomeGenerator {
* @param localZ The Z coordinate of the column within the chunk. * @param localZ The Z coordinate of the column within the chunk.
* @param mapper The island mapper to be used. * @param mapper The island mapper to be used.
* @param tempGenerator The temperature generator to be used. * @param tempGenerator The temperature generator to be used.
* @param biomeCache Cache for biomes.
* @param chunkGenCache Cache for whether or not the chunk is generated.
*/ */
public void generateBiomeColumn(Biome[][] biomesArray, World world, int chunkX, int chunkZ, int localX, int localZ, IslandWorldMapper mapper, BiomeSelector biomeSelector, TemperatureMapGenerator tempGenerator); public void generateBiomeColumn(Biome[][] biomesArray, World world, int chunkX, int chunkZ, int localX, int localZ, IslandWorldMapper mapper, BiomeSelector biomeSelector, TemperatureMapGenerator tempGenerator, Cache<Point2, Biome> biomeCache, Cache<Point2, Boolean> chunkGenCache);
/**
* Called when Bukkit designates a new chunk as generated.
* @param chunkX The X coordinate of the chunk.
* @param chunkZ The Z coordinate of the chunk.
*/
public void chunkGenerated(int chunkX, int chunkZ);
} }

View File

@ -7,43 +7,34 @@ import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.generator.ChunkGenerator; import org.bukkit.generator.ChunkGenerator;
import ca.recrown.islandsurvivalcraft.Utilities; import ca.recrown.islandsurvivalcraft.Utilities;
import ca.recrown.islandsurvivalcraft.caching.Cache;
import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.world.BiomeSelector; import ca.recrown.islandsurvivalcraft.world.BiomeSelector;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper; import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
import ca.recrown.islandsurvivalcraft.world.shaders.WorldHeightShader; import ca.recrown.islandsurvivalcraft.world.shaders.WorldHeightShader;
public class IslandWorldChunkGenerator extends ChunkGenerator implements Listener { public class IslandWorldChunkGenerator extends ChunkGenerator implements Listener {
private final BiomeGenerator biomeGenerator; private final Cache<Point2, Double> blockValueCache = new Cache<>(102400);
private final TemperatureMapGenerator temperatureGenerator = new TemperatureMapGenerator(); private final Cache<Point2, Biome> biomeCache = new Cache<>(102400);
private volatile Random random; private final Cache<Point2, Boolean> chunkExistenceCache = new Cache<>(16384);
private volatile IslandWorldMapper mapper; private volatile World currentWorld;
private volatile BiomeSelector biomeSelector;
private volatile World world;
private volatile WorldHeightShader heightShader;
public IslandWorldChunkGenerator(BiomeGenerator biomeGenerator) {
this.biomeGenerator = biomeGenerator;
}
public IslandWorldChunkGenerator() {
this.biomeGenerator = new UniBiomeIslandGenerator();
}
@Override @Override
public ChunkData generateChunkData(World world, Random random, int x, int z, BiomeGrid biome) { public ChunkData generateChunkData(World world, Random random, int x, int z, BiomeGrid biome) {
if (this.random != random) { this.currentWorld = world;
this.random = random; IslandWorldMapper mapper = new IslandWorldMapper(random, blockValueCache);
mapper = new IslandWorldMapper(random); TemperatureMapGenerator temperatureMapGenerator = new TemperatureMapGenerator(world.getSeed());
temperatureGenerator.setSeed(world.getSeed()); BiomeSelector biomeSelector = new BiomeSelector(random);
biomeSelector = new BiomeSelector(random);
biomeSelector.initialize(); biomeSelector.initialize();
this.world = world; WorldHeightShader heightShader = new WorldHeightShader(world.getSeed(), mapper, world.getSeaLevel(), world.getMaxHeight(), 3);
this.heightShader = new WorldHeightShader(world.getSeed(), mapper, world.getSeaLevel(), world.getMaxHeight(), 3); BiomeGenerator biomeGenerator = new UniBiomeIslandGenerator();
}
int maxHeight = world.getMaxHeight(); int maxHeight = world.getMaxHeight();
ChunkData chunkData = createChunkData(world); ChunkData chunkData = createChunkData(world);
@ -51,7 +42,7 @@ public class IslandWorldChunkGenerator extends ChunkGenerator implements Listene
for (int localX = 0; localX < Utilities.CHUNK_SIZE; localX++) { for (int localX = 0; localX < Utilities.CHUNK_SIZE; localX++) {
for (int localZ = 0; localZ < Utilities.CHUNK_SIZE; localZ++) { for (int localZ = 0; localZ < Utilities.CHUNK_SIZE; localZ++) {
if (biomes[localX][localZ] == null) { if (biomes[localX][localZ] == null) {
biomeGenerator.generateBiomeColumn(biomes, world, x, z, localX, localZ, mapper, biomeSelector, temperatureGenerator); biomeGenerator.generateBiomeColumn(biomes, world, x, z, localX, localZ, mapper, biomeSelector, temperatureMapGenerator, biomeCache, chunkExistenceCache);
} }
if (biomes[localX][localZ] == null) throw new IllegalStateException("Biome was null."); if (biomes[localX][localZ] == null) throw new IllegalStateException("Biome was null.");
for (int y = 0; y < maxHeight; y++) { for (int y = 0; y < maxHeight; y++) {
@ -68,12 +59,13 @@ public class IslandWorldChunkGenerator extends ChunkGenerator implements Listene
return chunkData; return chunkData;
} }
@EventHandler @EventHandler(priority = EventPriority.MONITOR)
public void onChunkLoaded(ChunkLoadEvent event) { public void onChunkLoaded(ChunkLoadEvent event) {
if (event.isNewChunk()) { if (event.isNewChunk()) {
if (world == event.getWorld()) { if (currentWorld == event.getWorld()) {
Chunk chunk = event.getChunk(); Chunk chunk = event.getChunk();
biomeGenerator.chunkGenerated(chunk.getX(), chunk.getZ());
chunkExistenceCache.set(new Point2(chunk.getX(), chunk.getZ()), true);
} }
} }
} }

View File

@ -4,7 +4,7 @@ import java.util.Random;
import org.bukkit.util.noise.SimplexOctaveGenerator; import org.bukkit.util.noise.SimplexOctaveGenerator;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.caching.Cache; import ca.recrown.islandsurvivalcraft.caching.Cache;
class TemperatureMapGenerator { class TemperatureMapGenerator {
@ -30,10 +30,10 @@ class TemperatureMapGenerator {
public float getTemperature(int worldX, int worldZ) { public float getTemperature(int worldX, int worldZ) {
Point2 loc = new Point2(worldX/4, worldZ/4); Point2 loc = new Point2(worldX/4, worldZ/4);
Float val = temperatureCache.getValue(loc); Float val = temperatureCache.get(loc);
if (val == null) { if (val == null) {
val = (float) noiseGenerator.noise(worldX/4, worldZ/4, frequency, amplitude, true); val = (float) noiseGenerator.noise(worldX/4, worldZ/4, frequency, amplitude, true);
temperatureCache.setValue(loc, val); temperatureCache.set(loc, val);
} }
return val; return val;
} }

View File

@ -4,27 +4,25 @@ import org.bukkit.World;
import org.bukkit.block.Biome; import org.bukkit.block.Biome;
import ca.recrown.islandsurvivalcraft.Utilities; import ca.recrown.islandsurvivalcraft.Utilities;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.caching.Cache; import ca.recrown.islandsurvivalcraft.caching.Cache;
import ca.recrown.islandsurvivalcraft.floodfill.Floodable;
import ca.recrown.islandsurvivalcraft.floodfill.Flooder;
import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateTargetValidatable; import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateTargetValidatable;
import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateValidatable;
import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch; import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch;
import ca.recrown.islandsurvivalcraft.world.BiomeSelector; import ca.recrown.islandsurvivalcraft.world.BiomeSelector;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper; import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
public class UniBiomeIslandGenerator implements BiomeGenerator { public class UniBiomeIslandGenerator implements BiomeGenerator {
private final Cache<Point2, Biome> biomeCache = new Cache<>(12800);
private final Cache<Point2, Boolean> existenceCache = new Cache<>(12800);
@Override @Override
public void generateBiomeColumn(Biome[][] biomes, World world, int chunkX, int chunkZ, int localX, int localZ, IslandWorldMapper mapper, BiomeSelector biomeSelector, TemperatureMapGenerator tempGen) { public void generateBiomeColumn(Biome[][] biomes, World world, int chunkX, int chunkZ, int localX, int localZ, IslandWorldMapper mapper, BiomeSelector biomeSelector, TemperatureMapGenerator tempGen, Cache<Point2, Biome> biomeCache, Cache<Point2, Boolean> chunkGenCache) {
int worldX = 16 * chunkX + localX; int worldX = 16 * chunkX + localX;
int worldZ = 16 * chunkZ + localZ; int worldZ = 16 * chunkZ + localZ;
Point2 chunkCoords = Utilities.worldToChunkCoordinates(new Point2(worldX, worldZ)); Point2 chunkCoords = Utilities.worldToChunkCoordinates(new Point2(worldX, worldZ));
existenceCache.setValue(chunkCoords, false); chunkGenCache.set(chunkCoords, false);
//Check if we can just give it something in cache. //Check if we can just give it something in cache.
Biome biome = getSavedBiome(world, worldX, worldZ); Biome biome = getSavedBiome(world, worldX, worldZ, biomeCache, chunkGenCache);
if (biome != null) { if (biome != null) {
biomes[localX][localZ] = biome; biomes[localX][localZ] = biome;
return; return;
@ -32,7 +30,7 @@ public class UniBiomeIslandGenerator implements BiomeGenerator {
//Fine, check if it's ocean. //Fine, check if it's ocean.
if (!mapper.isIsland(worldX, worldZ)) { if (!mapper.isIsland(worldX, worldZ)) {
setCacheBiome(worldX, worldZ, biomeSelector.getOceanBiome(tempGen.getTemperature(worldX, worldZ)), biomes); setCacheBiome(worldX, worldZ, biomeSelector.getOceanBiome(tempGen.getTemperature(worldX, worldZ)), biomes, biomeCache);
return; return;
} }
@ -40,7 +38,7 @@ public class UniBiomeIslandGenerator implements BiomeGenerator {
DepthFirstSearch search = new DepthFirstSearch(); DepthFirstSearch search = new DepthFirstSearch();
search.setValidatable(mapper); search.setValidatable(mapper);
IslandInfo islandInfo = new IslandInfo(mapper, world); IslandInfo islandInfo = new IslandInfo(mapper, world, biomeCache, chunkGenCache);
search.setStartPosition(worldX, worldZ); search.setStartPosition(worldX, worldZ);
if (!search.findTarget(islandInfo)) { if (!search.findTarget(islandInfo)) {
float temp = tempGen.getTemperature(worldX, worldZ); float temp = tempGen.getTemperature(worldX, worldZ);
@ -49,105 +47,105 @@ public class UniBiomeIslandGenerator implements BiomeGenerator {
if (islandInfo.shallow == null) islandInfo.shallow = biomeSelector.getOceanBiome(temp); if (islandInfo.shallow == null) islandInfo.shallow = biomeSelector.getOceanBiome(temp);
} }
PropagatorInfo propInfo = new PropagatorInfo(islandInfo, biomes, new Point2(chunkX, chunkZ), mapper); PropagatorInfo propInfo = new PropagatorInfo(islandInfo, biomes, new Point2(chunkX, chunkZ), mapper, biomeCache);
search.setValidatable(propInfo); Flooder flooder = new Flooder(new Point2(worldX, worldZ), propInfo);
search.setStartPosition(worldX, worldZ); flooder.start();
search.findTarget(propInfo);
} }
private Biome getSavedBiome(World world, int worldX, int worldZ) { private Biome getSavedBiome(World world, int worldX, int worldZ, Cache<Point2, Biome> biomeCache, Cache<Point2, Boolean> existenceCache) {
Point2 worldCoords = new Point2(worldX, worldZ); Point2 worldCoords = new Point2(worldX, worldZ);
Biome res = null; Biome res = null;
res = biomeCache.getValue(worldCoords); res = biomeCache.get(worldCoords);
if (res != null) return res; if (res != null) return res;
Point2 chunkCoords = Utilities.worldToChunkCoordinates(worldCoords); Point2 chunkCoords = Utilities.worldToChunkCoordinates(worldCoords);
Boolean chunkExists = existenceCache.getValue(chunkCoords); Boolean chunkExists = existenceCache.get(chunkCoords);
if (chunkExists == null) { if (chunkExists == null) {
chunkExists = world.isChunkGenerated(worldX, worldZ); chunkExists = world.isChunkGenerated(worldX, worldZ);
existenceCache.setValue(chunkCoords, chunkExists); existenceCache.set(chunkCoords, chunkExists);
} }
if (chunkExists) { if (chunkExists) {
res = world.getBiome(worldX, 0, worldZ); res = world.getBiome(worldX, 0, worldZ);
biomeCache.setValue(worldCoords, res); biomeCache.set(worldCoords, res);
} }
return res; return res;
} }
public void chunkGenerated(int chunkX, int chunkZ) { private void setCacheBiome(int worldX, int worldZ, Biome biome, Biome[][] localBiomes, Cache<Point2, Biome> biomeCache) {
existenceCache.setValue(new Point2(chunkX, chunkZ), true);
}
private void setCacheBiome(int worldX, int worldZ, Biome biome, Biome[][] biomes) {
Point2 worldCoords = new Point2(worldX, worldZ); Point2 worldCoords = new Point2(worldX, worldZ);
if (biomes != null) { if (localBiomes != null) {
Point2 localCoords = Utilities.worldToLocalChunkCoordinates(worldCoords); Point2 localCoords = Utilities.worldToLocalChunkCoordinates(worldCoords);
biomes[localCoords.x][localCoords.y] = biome; localBiomes[localCoords.x][localCoords.y] = biome;
} }
biomeCache.setValue(worldCoords, biome); biomeCache.set(worldCoords, biome);
} }
private class IslandInfo implements CoordinateTargetValidatable { private class IslandInfo implements CoordinateTargetValidatable {
public final IslandWorldMapper mapper; public final IslandWorldMapper mapper;
public final World world; public final World world;
private final Cache<Point2, Biome> biomeCache;
private final Cache<Point2, Boolean> chunkGenCache;
public Biome main, shore, shallow; public Biome main, shore, shallow;
public IslandInfo(IslandWorldMapper mapper, World world) { public IslandInfo(IslandWorldMapper mapper, World world, Cache<Point2, Biome> biomeCache, Cache<Point2, Boolean> chunkGenCache) {
this.mapper = mapper; this.mapper = mapper;
this.world = world; this.world = world;
this.biomeCache = biomeCache;
this.chunkGenCache = chunkGenCache;
} }
@Override @Override
public boolean isCoordinateTarget(int x, int y) { public boolean isCoordinateTarget(int x, int y) {
if (mapper.isLand(x, y)) { if (mapper.isLand(x, y)) {
if (mapper.isShore(x, y)) { if (mapper.isShore(x, y)) {
shore = getSavedBiome(world, x, y); shore = getSavedBiome(world, x, y, biomeCache, chunkGenCache);
} else { } else {
main = getSavedBiome(world, x, y); main = getSavedBiome(world, x, y, biomeCache, chunkGenCache);
} }
} else { } else {
shallow = getSavedBiome(world, x, y); shallow = getSavedBiome(world, x, y, biomeCache, chunkGenCache);
} }
return main != null && shore != null && shallow != null; return main != null && shore != null && shallow != null;
} }
} }
private class PropagatorInfo implements CoordinateTargetValidatable, CoordinateValidatable { private class PropagatorInfo implements Floodable {
private final Biome shallow, shore, main; private final Biome shallow, shore, main;
private final Biome[][] biomes; private final Biome[][] biomes;
private final Point2 chunkCoords; private final Point2 chunkCoords;
private final IslandWorldMapper mapper; private final IslandWorldMapper mapper;
private final Cache<Point2, Biome> biomeCache;
public PropagatorInfo(IslandInfo islandInfo, Biome[][] biomes, Point2 chunkCoords, IslandWorldMapper mapper) { public PropagatorInfo(IslandInfo islandInfo, Biome[][] biomes, Point2 chunkCoords, IslandWorldMapper mapper, Cache<Point2, Biome> biomeCache) {
this.shallow = islandInfo.shallow; this.shallow = islandInfo.shallow;
this.shore = islandInfo.shore; this.shore = islandInfo.shore;
this.main = islandInfo.main; this.main = islandInfo.main;
this.biomes = biomes; this.biomes = biomes;
this.chunkCoords = chunkCoords; this.chunkCoords = chunkCoords;
this.mapper = mapper; this.mapper = mapper;
this.biomeCache = biomeCache;
} }
@Override @Override
public boolean validate(int x, int y) { public boolean flood(Point2 point) {
Point2 chunkCoords = Utilities.worldToChunkCoordinates(new Point2(x, y)); Point2 chunkCoords = Utilities.worldToChunkCoordinates(point);
return this.chunkCoords.fastEquals(chunkCoords) && mapper.validate(x, y); if (!this.chunkCoords.fastEquals(chunkCoords) || !mapper.validate(point.x, point.y)) return false;
} int x = point.x;
int y = point.y;
@Override
public boolean isCoordinateTarget(int x, int y) {
if (mapper.isLand(x, y)) { if (mapper.isLand(x, y)) {
if (mapper.isShore(x, y)) { if (mapper.isShore(x, y)) {
setCacheBiome(x, y, shore, biomes); setCacheBiome(x, y, shore, biomes, biomeCache);
} else { } else {
setCacheBiome(x, y, main, biomes); setCacheBiome(x, y, main, biomes, biomeCache);
} }
} else { } else {
setCacheBiome(x, y, shallow, biomes); setCacheBiome(x, y, shallow, biomes, biomeCache);
} }
return false;
return true;
} }
} }

View File

@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestInstance.Lifecycle;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
/** /**
* Unit test for simple App. * Unit test for simple App.

View File

@ -2,9 +2,14 @@ package ca.recrown.islandsurvivalcraft.caching;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@ -28,35 +33,35 @@ public class CacheTest {
@Test @Test
public void testBasicCaching() { public void testBasicCaching() {
String val = integerCache.getValue(0); String val = integerCache.get(0);
assertTrue(val == null); assertTrue(val == null);
val = "first"; val = "first";
integerCache.setValue(0, val); integerCache.set(0, val);
assertTrue(integerCache.getValue(0) != null); assertTrue(integerCache.get(0) != null);
assertSame(val, integerCache.getValue(0)); assertSame(val, integerCache.get(0));
val = integerCache.getValue(1); val = integerCache.get(1);
assertTrue(val == null); assertTrue(val == null);
val = "second"; val = "second";
integerCache.setValue(1, val); integerCache.set(1, val);
assertTrue(integerCache.getValue(1) != null); assertTrue(integerCache.get(1) != null);
assertSame(val, integerCache.getValue(1)); assertSame(val, integerCache.get(1));
} }
@Test @Test
public void testUsageBasedClean() { public void testUsageBasedClean() {
integerCache.setValue(0, "first"); integerCache.set(0, "first");
integerCache.setValue(1, "second"); integerCache.set(1, "second");
integerCache.setValue(2, "third"); integerCache.set(2, "third");
assertEquals("first", integerCache.getValue(0)); assertEquals("first", integerCache.get(0));
integerCache.setValue(3, "fourth"); integerCache.set(3, "fourth");
assertEquals("first", integerCache.getValue(0)); assertEquals("first", integerCache.get(0));
integerCache.setValue(4, "fifth"); integerCache.set(4, "fifth");
assertTrue(integerCache.getValue(3) != null); assertTrue(integerCache.get(3) != null);
assertTrue(integerCache.getValue(0) != null); assertTrue(integerCache.get(0) != null);
} }
@Test @Test
@ -64,20 +69,20 @@ public class CacheTest {
int amount = 1024; int amount = 1024;
Random random = new Random(); Random random = new Random();
Cache<Integer, Integer> largeCache = new Cache<>(amount/2); Cache<Integer, Integer> largeCache = new Cache<>(amount / 2);
int[] values = new int[amount]; int[] values = new int[amount];
for (int i = 0; i < amount; i++) { for (int i = 0; i < amount; i++) {
values[i] = random.nextInt(); values[i] = random.nextInt();
largeCache.setValue(i, values[i]); largeCache.set(i, values[i]);
} }
for (int i = 0; i < amount /2; i++) { for (int i = 0; i < amount / 2; i++) {
assertEquals(null, largeCache.getValue(i), "Current accessor: " + i); assertEquals(null, largeCache.get(i), "Current accessor: " + i);
} }
for (int i = amount /2; i < amount; i++) { for (int i = amount / 2; i < amount; i++) {
assertEquals(values[i], largeCache.getValue(i), "Current accessor: " + i); assertEquals(values[i], largeCache.get(i), "Current accessor: " + i);
} }
} }
@ -86,23 +91,194 @@ public class CacheTest {
int amount = 1024; int amount = 1024;
Random random = new Random(); Random random = new Random();
Cache<Integer, Integer> largeCache = new Cache<>(amount/2); Cache<Integer, Integer> largeCache = new Cache<>(amount / 2);
int[] values = new int[amount]; int[] values = new int[amount];
for (int i = 0; i < amount; i++) { for (int i = 0; i < amount; i++) {
values[i] = random.nextInt(); values[i] = random.nextInt();
largeCache.setValue(i, values[i]); largeCache.set(i, values[i]);
largeCache.getValue(0); largeCache.get(0);
} }
for (int i = 1; i < (amount/2) + 1; i++) { for (int i = 1; i < (amount / 2) + 1; i++) {
assertEquals(null, largeCache.getValue(i), "Current accessor: " + i); assertEquals(null, largeCache.get(i), "Current accessor: " + i);
} }
for (int i = (amount /2) + 1; i < amount; i++) { for (int i = (amount / 2) + 1; i < amount; i++) {
assertEquals(values[i], largeCache.getValue(i), "Current accessor: " + i); assertEquals(values[i], largeCache.get(i), "Current accessor: " + i);
} }
assertEquals(values[0], largeCache.getValue(0)); assertEquals(values[0], largeCache.get(0));
}
@Test
public void testMultithreadingWriteConsistency() {
int size = 16384;
int[] answers = new int[size];
Random rand = new Random();
for (int i = 0; i < answers.length; i++) {
answers[i] = rand.nextInt();
}
Cache<Integer, Integer> lCache = new Cache<>(size);
Runnable write = new Runnable() {
@Override
public void run() {
for (int i = 0; i < size; i++) {
lCache.set(i, answers[i]);
}
}
};
Thread firstThread = new Thread(write);
firstThread.start();
Thread secondThread = new Thread(write);
secondThread.start();
Thread thirdThread = new Thread(write);
thirdThread.start();
try {
firstThread.join();
secondThread.join();
thirdThread.join();
} catch (InterruptedException e) {
assertFalse(false, e.getCause().getMessage());
}
for (int i = 0; i < size; i++) {
assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
}
}
@Test
public void testMultithreadingReadConsistency() {
int size = 16384;
Cache<Integer, Integer> lCache = new Cache<>(size);
int[] answers = new int[size];
Random rand = new Random();
for (int i = 0; i < answers.length; i++) {
answers[i] = rand.nextInt();
lCache.set(i, answers[i]);
}
Runnable read = new Runnable() {
@Override
public void run() {
for (int i = 0; i < size; i++) {
assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
}
}
};
Thread firstThread = new Thread(read);
firstThread.start();
Thread secondThread = new Thread(read);
secondThread.start();
Thread thirdThread = new Thread(read);
thirdThread.start();
try {
firstThread.join();
secondThread.join();
thirdThread.join();
} catch (InterruptedException e) {
assertFalse(false, e.getCause().getMessage());
}
}
@Test
public void testMulthreadedReadWrite() {
int size = 16384;
Cache<Integer, Integer> lCache = new Cache<>(size);
int[] answers = new int[size];
Random rand = new Random();
for (int i = 0; i < answers.length; i++) {
answers[i] = rand.nextInt();
lCache.set(i, answers[i]);
}
Runnable readWrite = new Runnable() {
@Override
public void run() {
for (int i = 0; i < size / 2; i++) {
if (lCache.get(i) == null) {
lCache.set(i, answers[i]);
} else {
assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
}
}
}
};
Runnable readAll = new Runnable() {
@Override
public void run() {
for (int i = 0; i < size; i++) {
assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
}
}
};
Thread firstThread = new Thread(readWrite);
firstThread.start();
Thread secondThread = new Thread(readWrite);
secondThread.start();
Thread thirdThread = new Thread(readAll);
try {
firstThread.join();
secondThread.join();
thirdThread.start();
thirdThread.join();
} catch (InterruptedException e) {
assertFalse(false, e.getCause().getMessage());
}
}
@Test
public void testMultiThreadConsistency() {
int size = 51200;
Cache<Integer, Integer> lCache = new Cache<>(size / 2);
int[] answers = new int[size];
Random rand = new Random();
ConcurrentHashMap<Integer, Integer> expectedStoredValues = new ConcurrentHashMap<>();
for (int i = 0; i < answers.length; i++) {
answers[i] = rand.nextInt();
lCache.set(i, answers[i]);
}
Runnable readWriteCheck = new Runnable(){
@Override
public void run() {
for (int i = 0; i < size; i++) {
if (lCache.get(i) != null) {
assertEquals(answers[i], lCache.get(i), "Accessor at: " + i);
} else {
lCache.set(i, answers[i]);
expectedStoredValues.put(i, answers[i]);
}
}
}
};
ExecutorService executorService = Executors.newFixedThreadPool(6);
executorService.submit(readWriteCheck);
executorService.submit(readWriteCheck);
executorService.submit(readWriteCheck);
executorService.submit(readWriteCheck);
executorService.submit(readWriteCheck);
executorService.submit(readWriteCheck);
try {
executorService.shutdown();
assertTrue(executorService.awaitTermination(1, TimeUnit.MINUTES), "Timed out.");
} catch (InterruptedException e) {
assertFalse(false, e.getCause().getMessage());
}
} }
} }

View File

@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestInstance.Lifecycle;
import ca.recrown.islandsurvivalcraft.types.Point2; import ca.recrown.islandsurvivalcraft.datatypes.Point2;
@TestInstance(Lifecycle.PER_CLASS) @TestInstance(Lifecycle.PER_CLASS)
public class FloodFillTest { public class FloodFillTest {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,109 @@
package ca.recrown.islandsurvivalcraft.world.generation;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.bukkit.block.Biome;
import org.junit.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import ca.recrown.islandsurvivalcraft.Utilities;
import ca.recrown.islandsurvivalcraft.caching.Cache;
import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.world.BiomeSelector;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
@TestInstance(Lifecycle.PER_CLASS)
public class UniBiomeIslandGeneratorTest {
private final int SEED = 249398015;
private final DummyWorld dummyWorld = new DummyWorld();
private final Cache<Point2, Double> blockValueCache = new Cache<>(102400);
private final Cache<Point2, Biome> biomeCache = new Cache<>(102400);
private final Cache<Point2, Boolean> chunkExistenceCache = new Cache<>(16384);
private final Random rand = new Random(SEED);
private class BiomeGenTask implements Runnable {
private final int amount;
private final int shift;
public BiomeGenTask(int amount, int shift) {
this.shift = shift;
this.amount = amount;
}
@Override
public void run() {
for (int x = 0; x < amount; x++) {
generateBiome(x, shift);
}
}
public void generateBiome(int chunkX, int chunkZ) {
IslandWorldMapper mapper = new IslandWorldMapper(rand, blockValueCache);
TemperatureMapGenerator temperatureMapGenerator = new TemperatureMapGenerator(SEED);
BiomeSelector biomeSelector = new BiomeSelector(rand);
biomeSelector.initialize();
BiomeGenerator biomeGenerator = new UniBiomeIslandGenerator();
Biome[][] biomes = new Biome[Utilities.CHUNK_SIZE][Utilities.CHUNK_SIZE];
for (int localX = 0; localX < Utilities.CHUNK_SIZE; localX++) {
for (int localZ = 0; localZ < Utilities.CHUNK_SIZE; localZ++) {
if (biomes[localX][localZ] == null) {
biomeGenerator.generateBiomeColumn(biomes, dummyWorld, chunkX, chunkZ, localX, localZ, mapper,
biomeSelector, temperatureMapGenerator, biomeCache, chunkExistenceCache);
}
if (biomes[localX][localZ] == null)
throw new IllegalStateException("Biome was null.");
}
}
}
}
@Test
public void testBiomeGenerationMultithread() {
int chunksToDo = 280;
Runnable g1 = new BiomeGenTask(chunksToDo, 0);
Runnable g2 = new BiomeGenTask(chunksToDo, 1);
Runnable g3 = new BiomeGenTask(chunksToDo, 2);
Runnable g4 = new BiomeGenTask(chunksToDo, 3);
Runnable g5 = new BiomeGenTask(chunksToDo, 4);
Runnable g6 = new BiomeGenTask(chunksToDo, 5);
ExecutorService ex = Executors.newFixedThreadPool(6);
ex.execute(g1);
ex.execute(g2);
ex.execute(g3);
ex.execute(g4);
ex.execute(g5);
ex.execute(g6);
try {
ex.shutdown();
assertTrue(ex.awaitTermination(1, TimeUnit.MINUTES), "timed out.");
} catch (InterruptedException e) {
assertFalse(false);
}
}
@Test
public void testBiomeGenerationSingleThread() {
Runnable g1 = new BiomeGenTask(1680, 0);
ExecutorService ex = Executors.newFixedThreadPool(6);
ex.execute(g1);
try {
ex.shutdown();
assertTrue(ex.awaitTermination(1, TimeUnit.MINUTES));
} catch (InterruptedException e) {
assertFalse(false, e.getCause().getMessage());
}
}
}