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
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "CodeLens (Launch) - UniBiomeIslandGeneratorTest",
"request": "launch",
"mainClass": "ca.recrown.islandsurvivalcraft.world.generation.UniBiomeIslandGeneratorTest",
"projectName": "IslandSurvivalCraft"
},
{
"type": "java",
"name": "Debug (Launch) - Current File",
@ -16,7 +23,9 @@
"request": "attach",
"hostName": "localhost",
"port": 25566,
"sourcePaths": ["src/main/java"],
"sourcePaths": [
"src/main/java"
],
"preLaunchTask": "start test server",
"postDebugTask": "stop test server"
}

14
pom.xml
View File

@ -11,9 +11,15 @@
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
</repository>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
@ -25,7 +31,7 @@
<artifactId>paper-api</artifactId>
<version>1.15.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependency>
</dependencies>
<build>
<plugins>
@ -41,6 +47,10 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
<properties>

View File

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

View File

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

View File

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

View File

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

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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,8 @@ package ca.recrown.islandsurvivalcraft.world.generation;
import org.bukkit.World;
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.IslandWorldMapper;
@ -13,6 +15,7 @@ public interface BiomeGenerator {
* 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.
* 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 world The current world.
* @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 mapper The island mapper 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);
/**
* 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);
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);
}

View File

@ -7,43 +7,34 @@ import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.generator.ChunkGenerator;
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;
import ca.recrown.islandsurvivalcraft.world.shaders.WorldHeightShader;
public class IslandWorldChunkGenerator extends ChunkGenerator implements Listener {
private final BiomeGenerator biomeGenerator;
private final TemperatureMapGenerator temperatureGenerator = new TemperatureMapGenerator();
private volatile Random random;
private volatile IslandWorldMapper mapper;
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();
}
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 volatile World currentWorld;
@Override
public ChunkData generateChunkData(World world, Random random, int x, int z, BiomeGrid biome) {
if (this.random != random) {
this.random = random;
mapper = new IslandWorldMapper(random);
temperatureGenerator.setSeed(world.getSeed());
biomeSelector = new BiomeSelector(random);
biomeSelector.initialize();
this.world = world;
this.heightShader = new WorldHeightShader(world.getSeed(), mapper, world.getSeaLevel(), world.getMaxHeight(), 3);
}
this.currentWorld = world;
IslandWorldMapper mapper = new IslandWorldMapper(random, blockValueCache);
TemperatureMapGenerator temperatureMapGenerator = new TemperatureMapGenerator(world.getSeed());
BiomeSelector biomeSelector = new BiomeSelector(random);
biomeSelector.initialize();
WorldHeightShader heightShader = new WorldHeightShader(world.getSeed(), mapper, world.getSeaLevel(), world.getMaxHeight(), 3);
BiomeGenerator biomeGenerator = new UniBiomeIslandGenerator();
int maxHeight = world.getMaxHeight();
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 localZ = 0; localZ < Utilities.CHUNK_SIZE; localZ++) {
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.");
for (int y = 0; y < maxHeight; y++) {
@ -68,12 +59,13 @@ public class IslandWorldChunkGenerator extends ChunkGenerator implements Listene
return chunkData;
}
@EventHandler
@EventHandler(priority = EventPriority.MONITOR)
public void onChunkLoaded(ChunkLoadEvent event) {
if (event.isNewChunk()) {
if (world == event.getWorld()) {
if (currentWorld == event.getWorld()) {
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 ca.recrown.islandsurvivalcraft.types.Point2;
import ca.recrown.islandsurvivalcraft.datatypes.Point2;
import ca.recrown.islandsurvivalcraft.caching.Cache;
class TemperatureMapGenerator {
@ -30,10 +30,10 @@ class TemperatureMapGenerator {
public float getTemperature(int worldX, int worldZ) {
Point2 loc = new Point2(worldX/4, worldZ/4);
Float val = temperatureCache.getValue(loc);
Float val = temperatureCache.get(loc);
if (val == null) {
val = (float) noiseGenerator.noise(worldX/4, worldZ/4, frequency, amplitude, true);
temperatureCache.setValue(loc, val);
temperatureCache.set(loc, val);
}
return val;
}

View File

@ -4,27 +4,25 @@ import org.bukkit.World;
import org.bukkit.block.Biome;
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.floodfill.Floodable;
import ca.recrown.islandsurvivalcraft.floodfill.Flooder;
import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateTargetValidatable;
import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateValidatable;
import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch;
import ca.recrown.islandsurvivalcraft.world.BiomeSelector;
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
public class UniBiomeIslandGenerator implements BiomeGenerator {
private final Cache<Point2, Biome> biomeCache = new Cache<>(12800);
private final Cache<Point2, Boolean> existenceCache = new Cache<>(12800);
@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 worldZ = 16 * chunkZ + localZ;
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.
Biome biome = getSavedBiome(world, worldX, worldZ);
Biome biome = getSavedBiome(world, worldX, worldZ, biomeCache, chunkGenCache);
if (biome != null) {
biomes[localX][localZ] = biome;
return;
@ -32,7 +30,7 @@ public class UniBiomeIslandGenerator implements BiomeGenerator {
//Fine, check if it's ocean.
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;
}
@ -40,7 +38,7 @@ public class UniBiomeIslandGenerator implements BiomeGenerator {
DepthFirstSearch search = new DepthFirstSearch();
search.setValidatable(mapper);
IslandInfo islandInfo = new IslandInfo(mapper, world);
IslandInfo islandInfo = new IslandInfo(mapper, world, biomeCache, chunkGenCache);
search.setStartPosition(worldX, worldZ);
if (!search.findTarget(islandInfo)) {
float temp = tempGen.getTemperature(worldX, worldZ);
@ -49,105 +47,105 @@ public class UniBiomeIslandGenerator implements BiomeGenerator {
if (islandInfo.shallow == null) islandInfo.shallow = biomeSelector.getOceanBiome(temp);
}
PropagatorInfo propInfo = new PropagatorInfo(islandInfo, biomes, new Point2(chunkX, chunkZ), mapper);
search.setValidatable(propInfo);
search.setStartPosition(worldX, worldZ);
search.findTarget(propInfo);
PropagatorInfo propInfo = new PropagatorInfo(islandInfo, biomes, new Point2(chunkX, chunkZ), mapper, biomeCache);
Flooder flooder = new Flooder(new Point2(worldX, worldZ), propInfo);
flooder.start();
}
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);
Biome res = null;
res = biomeCache.getValue(worldCoords);
res = biomeCache.get(worldCoords);
if (res != null) return res;
Point2 chunkCoords = Utilities.worldToChunkCoordinates(worldCoords);
Boolean chunkExists = existenceCache.getValue(chunkCoords);
Boolean chunkExists = existenceCache.get(chunkCoords);
if (chunkExists == null) {
chunkExists = world.isChunkGenerated(worldX, worldZ);
existenceCache.setValue(chunkCoords, chunkExists);
existenceCache.set(chunkCoords, chunkExists);
}
if (chunkExists) {
res = world.getBiome(worldX, 0, worldZ);
biomeCache.setValue(worldCoords, res);
biomeCache.set(worldCoords, res);
}
return res;
}
public void chunkGenerated(int chunkX, int chunkZ) {
existenceCache.setValue(new Point2(chunkX, chunkZ), true);
}
private void setCacheBiome(int worldX, int worldZ, Biome biome, Biome[][] biomes) {
private void setCacheBiome(int worldX, int worldZ, Biome biome, Biome[][] localBiomes, Cache<Point2, Biome> biomeCache) {
Point2 worldCoords = new Point2(worldX, worldZ);
if (biomes != null) {
if (localBiomes != null) {
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 {
public final IslandWorldMapper mapper;
public final World world;
private final Cache<Point2, Biome> biomeCache;
private final Cache<Point2, Boolean> chunkGenCache;
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.world = world;
this.biomeCache = biomeCache;
this.chunkGenCache = chunkGenCache;
}
@Override
public boolean isCoordinateTarget(int x, int y) {
if (mapper.isLand(x, y)) {
if (mapper.isShore(x, y)) {
shore = getSavedBiome(world, x, y);
shore = getSavedBiome(world, x, y, biomeCache, chunkGenCache);
} else {
main = getSavedBiome(world, x, y);
main = getSavedBiome(world, x, y, biomeCache, chunkGenCache);
}
} else {
shallow = getSavedBiome(world, x, y);
shallow = getSavedBiome(world, x, y, biomeCache, chunkGenCache);
}
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[][] biomes;
private final Point2 chunkCoords;
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.shore = islandInfo.shore;
this.main = islandInfo.main;
this.biomes = biomes;
this.chunkCoords = chunkCoords;
this.mapper = mapper;
this.biomeCache = biomeCache;
}
@Override
public boolean validate(int x, int y) {
Point2 chunkCoords = Utilities.worldToChunkCoordinates(new Point2(x, y));
return this.chunkCoords.fastEquals(chunkCoords) && mapper.validate(x, y);
}
@Override
public boolean isCoordinateTarget(int x, int y) {
public boolean flood(Point2 point) {
Point2 chunkCoords = Utilities.worldToChunkCoordinates(point);
if (!this.chunkCoords.fastEquals(chunkCoords) || !mapper.validate(point.x, point.y)) return false;
int x = point.x;
int y = point.y;
if (mapper.isLand(x, y)) {
if (mapper.isShore(x, y)) {
setCacheBiome(x, y, shore, biomes);
setCacheBiome(x, y, shore, biomes, biomeCache);
} else {
setCacheBiome(x, y, main, biomes);
setCacheBiome(x, y, main, biomes, biomeCache);
}
} 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.Lifecycle;
import ca.recrown.islandsurvivalcraft.types.Point2;
import ca.recrown.islandsurvivalcraft.datatypes.Point2;
/**
* 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.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
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.BeforeAll;
@ -28,35 +33,35 @@ public class CacheTest {
@Test
public void testBasicCaching() {
String val = integerCache.getValue(0);
String val = integerCache.get(0);
assertTrue(val == null);
val = "first";
integerCache.setValue(0, val);
assertTrue(integerCache.getValue(0) != null);
assertSame(val, integerCache.getValue(0));
integerCache.set(0, val);
assertTrue(integerCache.get(0) != null);
assertSame(val, integerCache.get(0));
val = integerCache.getValue(1);
val = integerCache.get(1);
assertTrue(val == null);
val = "second";
integerCache.setValue(1, val);
assertTrue(integerCache.getValue(1) != null);
assertSame(val, integerCache.getValue(1));
integerCache.set(1, val);
assertTrue(integerCache.get(1) != null);
assertSame(val, integerCache.get(1));
}
@Test
public void testUsageBasedClean() {
integerCache.setValue(0, "first");
integerCache.setValue(1, "second");
integerCache.setValue(2, "third");
assertEquals("first", integerCache.getValue(0));
integerCache.set(0, "first");
integerCache.set(1, "second");
integerCache.set(2, "third");
integerCache.setValue(3, "fourth");
assertEquals("first", integerCache.getValue(0));
assertEquals("first", integerCache.get(0));
integerCache.setValue(4, "fifth");
assertTrue(integerCache.getValue(3) != null);
assertTrue(integerCache.getValue(0) != null);
integerCache.set(3, "fourth");
assertEquals("first", integerCache.get(0));
integerCache.set(4, "fifth");
assertTrue(integerCache.get(3) != null);
assertTrue(integerCache.get(0) != null);
}
@Test
@ -64,20 +69,20 @@ public class CacheTest {
int amount = 1024;
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];
for (int i = 0; i < amount; i++) {
values[i] = random.nextInt();
largeCache.setValue(i, values[i]);
largeCache.set(i, values[i]);
}
for (int i = 0; i < amount /2; i++) {
assertEquals(null, largeCache.getValue(i), "Current accessor: " + i);
for (int i = 0; i < amount / 2; i++) {
assertEquals(null, largeCache.get(i), "Current accessor: " + i);
}
for (int i = amount /2; i < amount; i++) {
assertEquals(values[i], largeCache.getValue(i), "Current accessor: " + i);
for (int i = amount / 2; i < amount; i++) {
assertEquals(values[i], largeCache.get(i), "Current accessor: " + i);
}
}
@ -86,23 +91,194 @@ public class CacheTest {
int amount = 1024;
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];
for (int i = 0; i < amount; i++) {
values[i] = random.nextInt();
largeCache.setValue(i, values[i]);
largeCache.getValue(0);
largeCache.set(i, values[i]);
largeCache.get(0);
}
for (int i = 1; i < (amount/2) + 1; i++) {
assertEquals(null, largeCache.getValue(i), "Current accessor: " + i);
for (int i = 1; i < (amount / 2) + 1; i++) {
assertEquals(null, largeCache.get(i), "Current accessor: " + i);
}
for (int i = (amount /2) + 1; i < amount; i++) {
assertEquals(values[i], largeCache.getValue(i), "Current accessor: " + i);
for (int i = (amount / 2) + 1; i < amount; 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.Lifecycle;
import ca.recrown.islandsurvivalcraft.types.Point2;
import ca.recrown.islandsurvivalcraft.datatypes.Point2;
@TestInstance(Lifecycle.PER_CLASS)
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());
}
}
}