Attempted at making all things world gen thread proof.
Specifically, caching is tested thread proof.
This commit is contained in:
parent
f20515fd45
commit
98e4265db7
@ -3,15 +3,13 @@ package ca.recrown.islandsurvivalcraft.Types;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class Point2 {
|
public class Point2 {
|
||||||
public int x, y;
|
public final int x, y;
|
||||||
|
private final int hash;
|
||||||
|
|
||||||
public Point2(int x, int y) {
|
public Point2(int x, int y) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
}
|
this.hash = Objects.hash(x, y);
|
||||||
|
|
||||||
public Point2() {
|
|
||||||
this(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -23,8 +21,12 @@ public class Point2 {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean fastEquals(Point2 point) {
|
||||||
|
return point.hashCode() == this.hash && x == point.x && y == point.y;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(x, y);
|
return hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,16 +2,19 @@ package ca.recrown.islandsurvivalcraft.caching;
|
|||||||
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
public class Cache<Key, Value> {
|
public class Cache<Key, Value> {
|
||||||
private final int maxSize;
|
private final int maxSize;
|
||||||
private final ConcurrentHashMap<Key, CacheValue<Value>> data;
|
private final ConcurrentHashMap<Key, CacheValue<Value>> data;
|
||||||
private final ConcurrentLinkedQueue<Key> occurrenceOrder;
|
private final ConcurrentLinkedQueue<Key> occurrenceOrder;
|
||||||
|
private ReentrantLock cleaningLock;
|
||||||
|
|
||||||
public Cache(int maxSize) {
|
public Cache(int maxSize) {
|
||||||
data = new ConcurrentHashMap<>(maxSize);
|
data = new ConcurrentHashMap<>(maxSize);
|
||||||
occurrenceOrder = new ConcurrentLinkedQueue<>();
|
occurrenceOrder = new ConcurrentLinkedQueue<>();
|
||||||
this.maxSize = maxSize;
|
this.maxSize = maxSize;
|
||||||
|
cleaningLock = new ReentrantLock(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cache() {
|
public Cache() {
|
||||||
@ -19,18 +22,27 @@ public class Cache<Key, Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setValue(Key key, Value value) {
|
public void setValue(Key key, Value value) {
|
||||||
data.put(key, new CacheValue<>(value));
|
CacheValue<Value> previous = data.get(key);
|
||||||
|
CacheValue<Value> fresh = new CacheValue<>(value);
|
||||||
|
if (previous != null) fresh.occurrence += previous.occurrence;
|
||||||
occurrenceOrder.add(key);
|
occurrenceOrder.add(key);
|
||||||
|
data.put(key, fresh);
|
||||||
|
|
||||||
if (data.size() > maxSize) {
|
if (data.size() > maxSize) {
|
||||||
int occ = 0;
|
int occ = 0;
|
||||||
do {
|
do {
|
||||||
|
cleaningLock.lock();
|
||||||
|
try {
|
||||||
Key potentialKey = occurrenceOrder.poll();
|
Key potentialKey = occurrenceOrder.poll();
|
||||||
CacheValue<Value> potential = data.get(potentialKey);
|
CacheValue<Value> potential = data.get(potentialKey);
|
||||||
potential.decreaseOccurence();
|
potential.occurrence--;
|
||||||
occ = potential.getOccurrence();
|
occ = potential.occurrence;
|
||||||
if (occ < 1) {
|
if (occ < 1) {
|
||||||
data.remove(potentialKey);
|
data.remove(potentialKey);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
cleaningLock.unlock();
|
||||||
|
}
|
||||||
} while (occ > 0);
|
} while (occ > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,12 +55,18 @@ public class Cache<Key, Value> {
|
|||||||
* @return the value associated to the key.
|
* @return the value associated to the key.
|
||||||
*/
|
*/
|
||||||
public Value getValue(Key key) {
|
public Value getValue(Key key) {
|
||||||
CacheValue<Value> res = data.get(key);
|
CacheValue<Value> res = null;
|
||||||
|
cleaningLock.lock();
|
||||||
|
try {
|
||||||
|
res = data.get(key);
|
||||||
if (res == null) return null;
|
if (res == null) return null;
|
||||||
|
res.occurrence++;
|
||||||
res.increaseOccurrence();
|
|
||||||
occurrenceOrder.add(key);
|
occurrenceOrder.add(key);
|
||||||
return res.getValue();
|
} finally {
|
||||||
|
cleaningLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
|
@ -1,32 +1,10 @@
|
|||||||
package ca.recrown.islandsurvivalcraft.caching;
|
package ca.recrown.islandsurvivalcraft.caching;
|
||||||
|
|
||||||
public class CacheValue <ValueType> {
|
class CacheValue<ValueType> {
|
||||||
private int occurrence = 1;
|
public volatile int occurrence = 1;
|
||||||
private final ValueType value;
|
public final ValueType data;
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the occurrence
|
|
||||||
*/
|
|
||||||
public int getOccurrence() {
|
|
||||||
return occurrence;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void increaseOccurrence() {
|
|
||||||
occurrence++;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void decreaseOccurence() {
|
|
||||||
occurrence--;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheValue(ValueType value) {
|
public CacheValue(ValueType value) {
|
||||||
this.value = value;
|
this.data = value;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the value
|
|
||||||
*/
|
|
||||||
public ValueType getValue() {
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,8 @@
|
|||||||
package ca.recrown.islandsurvivalcraft.world;
|
package ca.recrown.islandsurvivalcraft.world;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
|
|
||||||
@ -13,19 +14,19 @@ import ca.recrown.islandsurvivalcraft.world.generation.IslandWorldGenerator;
|
|||||||
* Uses IslandWorldGenerator as the container for each world.
|
* Uses IslandWorldGenerator as the container for each world.
|
||||||
*/
|
*/
|
||||||
public class IslandWorldGeneratorAlternator {
|
public class IslandWorldGeneratorAlternator {
|
||||||
private HashMap<Long, IslandWorldGenerator> chunkGenerator;
|
private final ConcurrentHashMap<UUID, IslandWorldGenerator> chunkGenerators;
|
||||||
private IslandBiomeGenerator islandBiomeGenerator;
|
private final IslandBiomeGenerator islandBiomeGenerator;
|
||||||
|
|
||||||
public IslandWorldGeneratorAlternator(IslandBiomeGenerator biomeGenerator) {
|
public IslandWorldGeneratorAlternator(IslandBiomeGenerator biomeGenerator) {
|
||||||
chunkGenerator = new HashMap<>();
|
this.chunkGenerators = new ConcurrentHashMap<>();
|
||||||
this.islandBiomeGenerator = biomeGenerator;
|
this.islandBiomeGenerator = biomeGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized IslandWorldGenerator getIslandChunkGeneratorSystem(World world, Random random) {
|
public IslandWorldGenerator getIslandChunkGeneratorSystem(World world, Random random) {
|
||||||
long tid = Thread.currentThread().getId();
|
UUID wUuid = world.getUID();
|
||||||
if (!chunkGenerator.containsKey(tid)) {
|
if (!chunkGenerators.containsKey(wUuid)) {
|
||||||
chunkGenerator.put(tid, new IslandWorldGenerator(world, islandBiomeGenerator.getInstance(), random));
|
chunkGenerators.put(wUuid, new IslandWorldGenerator(world, islandBiomeGenerator.getInstance(), random));
|
||||||
}
|
}
|
||||||
return chunkGenerator.get(tid);
|
return chunkGenerators.get(wUuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,9 +10,9 @@ import ca.recrown.islandsurvivalcraft.pathfinding.CoordinateValidatable;
|
|||||||
import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch;
|
import ca.recrown.islandsurvivalcraft.pathfinding.DepthFirstSearch;
|
||||||
|
|
||||||
public class IslandWorldMapper implements CoordinateValidatable {
|
public class IslandWorldMapper implements CoordinateValidatable {
|
||||||
private Cache<Point2, Double> blockValueCache;
|
private final Cache<Point2, Double> blockValueCache;
|
||||||
|
|
||||||
private SimplexOctaveGenerator noiseGenerator;
|
private final SimplexOctaveGenerator noiseGenerator;
|
||||||
private final int noiseOctaves = 4;
|
private final int noiseOctaves = 4;
|
||||||
private final float islandBlockGenerationPercent = 16;
|
private final float islandBlockGenerationPercent = 16;
|
||||||
private final float exaggerationFactor = 1.2f;
|
private final float exaggerationFactor = 1.2f;
|
||||||
@ -28,7 +28,7 @@ public class IslandWorldMapper implements CoordinateValidatable {
|
|||||||
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<>(4096);
|
blockValueCache = new Cache<>(32768);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,6 +113,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.getValue(p);
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
double portionSea = 1f - (this.islandBlockGenerationPercent / 100f);
|
double portionSea = 1f - (this.islandBlockGenerationPercent / 100f);
|
||||||
|
@ -5,7 +5,7 @@ import java.util.Random;
|
|||||||
import org.bukkit.util.noise.SimplexOctaveGenerator;
|
import org.bukkit.util.noise.SimplexOctaveGenerator;
|
||||||
|
|
||||||
public class BedrockGenerator {
|
public class BedrockGenerator {
|
||||||
private SimplexOctaveGenerator noiseGenerator;
|
private final SimplexOctaveGenerator noiseGenerator;
|
||||||
private final int maxBedrockHeight;
|
private final int maxBedrockHeight;
|
||||||
private final int minBedrockHeight;
|
private final int minBedrockHeight;
|
||||||
|
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package ca.recrown.islandsurvivalcraft.world.generation;
|
package ca.recrown.islandsurvivalcraft.world.generation;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Biome;
|
import org.bukkit.block.Biome;
|
||||||
|
|
||||||
@ -16,32 +14,31 @@ import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
|
|||||||
|
|
||||||
//Note: technically, the validators have to be run on land, and so, some condition checks may not be nessecary.
|
//Note: technically, the validators have to be run on land, and so, some condition checks may not be nessecary.
|
||||||
public class BiomePerIslandGenerator implements IslandBiomeGenerator {
|
public class BiomePerIslandGenerator implements IslandBiomeGenerator {
|
||||||
private boolean initialized;
|
private volatile boolean initialized;
|
||||||
private final TemperatureMapGenerator temperatureMapGenerator;
|
private final TemperatureMapGenerator temperatureMapGenerator;
|
||||||
private final Cache<Point2, Biome[][]> chunkBiomesCache;
|
private final Cache<Point2, Biome[][]> chunkBiomesCache;
|
||||||
private final Cache<Point2, Boolean> chunkGenStatusCache;
|
private final Cache<Point2, Boolean> chunkGenStatusCache;
|
||||||
private IslandWorldMapper worldIslandMap;
|
private volatile IslandWorldMapper worldIslandMap;
|
||||||
private BiomeSelector biomeSelector;
|
private volatile BiomeSelector biomeSelector;
|
||||||
private World world;
|
private volatile World world;
|
||||||
private final DepthFirstSearch freshCachePropagator;
|
private final DepthFirstSearch freshCachePropagator;
|
||||||
private final DepthFirstSearch existenceChecker;
|
private final DepthFirstSearch existenceChecker;
|
||||||
private FreshCachePropagationInfo freshCachePropInfo;
|
private final FreshCachePropagationInfo freshCachePropInfo;
|
||||||
private PreviousGenerationInfo existenceInfo;
|
private final PreviousGenerationInfo existenceInfo;
|
||||||
private Point2 currChunkCoords;
|
private volatile Point2 currChunkCoords;
|
||||||
private final Biome[][] localChunkCache;
|
private volatile Biome[][] localChunkCache;
|
||||||
|
|
||||||
float temperature;
|
float temperature;
|
||||||
|
|
||||||
public BiomePerIslandGenerator() {
|
public BiomePerIslandGenerator() {
|
||||||
this.temperatureMapGenerator = new TemperatureMapGenerator();
|
this.temperatureMapGenerator = new TemperatureMapGenerator();
|
||||||
chunkBiomesCache = new Cache<>(1024);
|
chunkBiomesCache = new Cache<>(512);
|
||||||
chunkGenStatusCache = new Cache<>(1024);
|
chunkGenStatusCache = new Cache<>(512);
|
||||||
freshCachePropInfo = new FreshCachePropagationInfo();
|
freshCachePropInfo = new FreshCachePropagationInfo();
|
||||||
freshCachePropagator = new DepthFirstSearch(freshCachePropInfo);
|
freshCachePropagator = new DepthFirstSearch(freshCachePropInfo);
|
||||||
existenceInfo = new PreviousGenerationInfo();
|
existenceInfo = new PreviousGenerationInfo();
|
||||||
existenceChecker = new DepthFirstSearch(existenceInfo);
|
existenceChecker = new DepthFirstSearch(existenceInfo);
|
||||||
localChunkCache = new Biome[16][16];
|
localChunkCache = new Biome[16][16];
|
||||||
currChunkCoords = new Point2();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -61,12 +58,9 @@ public class BiomePerIslandGenerator implements IslandBiomeGenerator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Biome GenerateBiome(int chunkX, int chunkZ, int localX, int localZ) {
|
public Biome GenerateBiome(int chunkX, int chunkZ, int localX, int localZ) {
|
||||||
if (chunkX != currChunkCoords.x || chunkZ != currChunkCoords.y) {
|
if (currChunkCoords == null || chunkX != currChunkCoords.x || chunkZ != currChunkCoords.y) {
|
||||||
currChunkCoords.x = chunkX;
|
currChunkCoords = new Point2(chunkX, chunkZ);
|
||||||
currChunkCoords.y = chunkZ;
|
localChunkCache = new Biome[16][16];
|
||||||
for (int i = 0; i < 16; i++) {
|
|
||||||
Arrays.fill(localChunkCache[i], null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
int worldX = Utilities.addMagnitude(16 * chunkX, localX);
|
int worldX = Utilities.addMagnitude(16 * chunkX, localX);
|
||||||
int worldZ = Utilities.addMagnitude(16 * chunkZ, localZ);
|
int worldZ = Utilities.addMagnitude(16 * chunkZ, localZ);
|
||||||
@ -96,7 +90,7 @@ public class BiomePerIslandGenerator implements IslandBiomeGenerator {
|
|||||||
int localZ = Math.abs(worldZ % 16);
|
int localZ = Math.abs(worldZ % 16);
|
||||||
Point2 chunkCoords = new Point2(worldX / 16, worldZ / 16);
|
Point2 chunkCoords = new Point2(worldX / 16, worldZ / 16);
|
||||||
|
|
||||||
if (chunkCoords.equals(this.currChunkCoords) && localChunkCache[localX][localZ] != null) {
|
if (chunkCoords.fastEquals(this.currChunkCoords) && localChunkCache[localX][localZ] != null) {
|
||||||
return localChunkCache[localX][localZ];
|
return localChunkCache[localX][localZ];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +121,7 @@ public class BiomePerIslandGenerator implements IslandBiomeGenerator {
|
|||||||
int localZ = Math.abs(worldZ % 16);
|
int localZ = Math.abs(worldZ % 16);
|
||||||
Point2 chunkCoords = new Point2(worldX / 16, worldZ / 16);
|
Point2 chunkCoords = new Point2(worldX / 16, worldZ / 16);
|
||||||
|
|
||||||
if (chunkCoords.equals(this.currChunkCoords)) localChunkCache[localX][localZ] = biome;
|
if (chunkCoords.fastEquals(this.currChunkCoords)) localChunkCache[localX][localZ] = biome;
|
||||||
|
|
||||||
Biome[][] chunkBiomes = chunkBiomesCache.getValue(chunkCoords);
|
Biome[][] chunkBiomes = chunkBiomesCache.getValue(chunkCoords);
|
||||||
if (chunkBiomes == null) chunkBiomes = new Biome[16][16];
|
if (chunkBiomes == null) chunkBiomes = new Biome[16][16];
|
||||||
@ -157,7 +151,7 @@ public class BiomePerIslandGenerator implements IslandBiomeGenerator {
|
|||||||
@Override
|
@Override
|
||||||
public boolean validate(int x, int y) {
|
public boolean validate(int x, int y) {
|
||||||
Point2 chunkCoords = new Point2(x / 16, y / 16);
|
Point2 chunkCoords = new Point2(x / 16, y / 16);
|
||||||
return chunkCoords.x == currChunkCoords.x && chunkCoords.y == currChunkCoords.y && worldIslandMap.isIsland(x, y);
|
return chunkCoords.fastEquals(currChunkCoords) && worldIslandMap.isIsland(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allBiomesAcquired() {
|
public boolean allBiomesAcquired() {
|
||||||
|
@ -9,6 +9,7 @@ import org.bukkit.block.Biome;
|
|||||||
import org.bukkit.generator.ChunkGenerator.BiomeGrid;
|
import org.bukkit.generator.ChunkGenerator.BiomeGrid;
|
||||||
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
import org.bukkit.generator.ChunkGenerator.ChunkData;
|
||||||
|
|
||||||
|
import ca.recrown.islandsurvivalcraft.Utilities;
|
||||||
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;
|
||||||
@ -44,8 +45,9 @@ public class IslandWorldGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void GenerateChunk(int chunkX, int chunkZ, int localX, int localZ, ChunkData chunk, BiomeGrid biomeGrid) {
|
public void GenerateChunk(int chunkX, int chunkZ, int localX, int localZ, ChunkData chunk, BiomeGrid biomeGrid) {
|
||||||
int worldX = 16 * chunkX + localX;
|
int worldX = Utilities.addMagnitude(16 * chunkX, localX);
|
||||||
int worldZ = 16 * chunkZ + localZ;
|
int worldZ = Utilities.addMagnitude(16 * chunkZ, localZ);
|
||||||
|
|
||||||
// gets the bedrock.
|
// gets the bedrock.
|
||||||
int bedrockHeight = bedrockGenerator.getBedrockHeight(worldX, worldZ);
|
int bedrockHeight = bedrockGenerator.getBedrockHeight(worldX, worldZ);
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ class TemperatureMapGenerator {
|
|||||||
private final double frequency = 0.5D;
|
private final double frequency = 0.5D;
|
||||||
private final double amplitude = 0.5D;
|
private final double amplitude = 0.5D;
|
||||||
|
|
||||||
private SimplexOctaveGenerator noiseGenerator;
|
private volatile SimplexOctaveGenerator noiseGenerator;
|
||||||
|
|
||||||
public TemperatureMapGenerator() {
|
public TemperatureMapGenerator() {
|
||||||
temperatureCache = new Cache<>(1024);
|
temperatureCache = new Cache<>(1024);
|
||||||
|
@ -9,12 +9,12 @@ import org.bukkit.util.noise.SimplexOctaveGenerator;
|
|||||||
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
|
import ca.recrown.islandsurvivalcraft.world.IslandWorldMapper;
|
||||||
|
|
||||||
public class WorldHeightShader {
|
public class WorldHeightShader {
|
||||||
private Random random;
|
private final Random random;
|
||||||
private SimplexOctaveGenerator noiseGenerator;
|
private final SimplexOctaveGenerator noiseGenerator;
|
||||||
private IslandWorldMapper islandLocator;
|
private final IslandWorldMapper islandLocator;
|
||||||
private int seaLevel;
|
private final int seaLevel;
|
||||||
private int worldHeight;
|
private final int worldHeight;
|
||||||
private int minimumHeight;
|
private final int minimumHeight;
|
||||||
private final float probabilityOfAdditive = 0.75f;
|
private final float probabilityOfAdditive = 0.75f;
|
||||||
|
|
||||||
public WorldHeightShader(long seed, IslandWorldMapper islandLocator, int seaLevel, int worldHeight, int minimumHeight) {
|
public WorldHeightShader(long seed, IslandWorldMapper islandLocator, int seaLevel, int worldHeight, int minimumHeight) {
|
||||||
|
Loading…
Reference in New Issue
Block a user