Cave liquid
This commit is contained in:
parent
6f6150475e
commit
43e9c2412d
@ -26,6 +26,9 @@ val Registry.Ref<TileDefinition>.isObjectPlatformTile: Boolean
|
|||||||
val Registry.Entry<TileDefinition>.isEmptyTile: Boolean
|
val Registry.Entry<TileDefinition>.isEmptyTile: Boolean
|
||||||
get() = this == BuiltinMetaMaterials.EMPTY || this == BuiltinMetaMaterials.NULL
|
get() = this == BuiltinMetaMaterials.EMPTY || this == BuiltinMetaMaterials.NULL
|
||||||
|
|
||||||
|
val Registry.Entry<TileDefinition>.isNotEmptyTile: Boolean
|
||||||
|
get() = !isEmptyTile
|
||||||
|
|
||||||
val Registry.Entry<TileDefinition>.isNullTile: Boolean
|
val Registry.Entry<TileDefinition>.isNullTile: Boolean
|
||||||
get() = this == BuiltinMetaMaterials.NULL
|
get() = this == BuiltinMetaMaterials.NULL
|
||||||
|
|
||||||
|
@ -553,9 +553,11 @@ class WorldLayout {
|
|||||||
|
|
||||||
val yi: Int
|
val yi: Int
|
||||||
|
|
||||||
if (y < layers.first().yStart) {
|
if (y == layers.first().yStart) {
|
||||||
|
yi = 0
|
||||||
|
} else if (y < layers.first().yStart) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
} else if (y > layers.last().yStart) {
|
} else if (y >= layers.last().yStart) {
|
||||||
yi = layers.size
|
yi = layers.size
|
||||||
} else {
|
} else {
|
||||||
yi = layers.indexOfFirst { it.yStart >= y } - 1
|
yi = layers.indexOfFirst { it.yStart >= y } - 1
|
||||||
|
@ -204,8 +204,16 @@ class WorldTemplate(val geometry: WorldGeometry) {
|
|||||||
var backgroundCave = false
|
var backgroundCave = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// making cache big enough to make
|
||||||
|
// serial generation stages fast enough,
|
||||||
|
// since sampling noise is costly
|
||||||
|
|
||||||
|
// TODO: Don't specify scheduler and executor since
|
||||||
|
// G1GC doesn't like this and will refuse to clean up
|
||||||
|
// memory retained by this cache until G1GC feels like it
|
||||||
|
// (needs more profiling)
|
||||||
private val cellCache = Caffeine.newBuilder()
|
private val cellCache = Caffeine.newBuilder()
|
||||||
.maximumSize(125_000L)
|
.maximumSize(1_000_000L)
|
||||||
.expireAfterAccess(Duration.ofMinutes(2))
|
.expireAfterAccess(Duration.ofMinutes(2))
|
||||||
.executor(Starbound.EXECUTOR)
|
.executor(Starbound.EXECUTOR)
|
||||||
.scheduler(Scheduler.systemScheduler())
|
.scheduler(Scheduler.systemScheduler())
|
||||||
|
@ -4,27 +4,31 @@ import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
|
|||||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.future.await
|
import kotlinx.coroutines.future.await
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.apache.logging.log4j.LogManager
|
import org.apache.logging.log4j.LogManager
|
||||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||||
|
import ru.dbotthepony.kommons.util.AABBi
|
||||||
import ru.dbotthepony.kommons.vector.Vector2d
|
import ru.dbotthepony.kommons.vector.Vector2d
|
||||||
import ru.dbotthepony.kommons.vector.Vector2i
|
import ru.dbotthepony.kommons.vector.Vector2i
|
||||||
import ru.dbotthepony.kstarbound.Starbound
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||||
|
import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
|
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
|
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
|
||||||
|
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
|
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
|
||||||
|
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
|
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
|
import ru.dbotthepony.kstarbound.defs.tile.supportsModifier
|
||||||
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
|
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
|
||||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
|
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
|
||||||
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
|
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
|
||||||
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
|
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
|
||||||
|
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
|
||||||
import ru.dbotthepony.kstarbound.world.Chunk
|
import ru.dbotthepony.kstarbound.world.Chunk
|
||||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||||
import ru.dbotthepony.kstarbound.world.IChunkListener
|
import ru.dbotthepony.kstarbound.world.IChunkListener
|
||||||
@ -132,11 +136,13 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
}
|
}
|
||||||
|
|
||||||
State.CAVE_LIQUID -> {
|
State.CAVE_LIQUID -> {
|
||||||
//LOGGER.error("NYI: Generating cave liquids for $chunk")
|
// not thread safe, but takes very little time to execute
|
||||||
|
generateLiquid()
|
||||||
}
|
}
|
||||||
|
|
||||||
State.FULL -> {
|
State.FULL -> {
|
||||||
CompletableFuture.runAsync(Runnable { placeGrass() }, Starbound.EXECUTOR).await()
|
// CompletableFuture.runAsync(Runnable { placeGrass() }, Starbound.EXECUTOR).await()
|
||||||
|
placeGrass()
|
||||||
}
|
}
|
||||||
|
|
||||||
State.FRESH -> throw RuntimeException()
|
State.FRESH -> throw RuntimeException()
|
||||||
@ -632,11 +638,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
val cellAbove = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y + 1)
|
val cellAbove = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y + 1)
|
||||||
val cellBelow = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y - 1)
|
val cellBelow = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y - 1)
|
||||||
|
|
||||||
val tileAbove = cell.tile(isBackground)
|
|
||||||
val tileBelow = cell.tile(isBackground)
|
|
||||||
val tileAboveInv = cell.tile(!isBackground)
|
|
||||||
val tileBelowInv = cell.tile(!isBackground)
|
|
||||||
|
|
||||||
val isFloor = (!cell.foreground.material.isEmptyTile && cellAbove.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellAbove.background.material.isEmptyTile)
|
val isFloor = (!cell.foreground.material.isEmptyTile && cellAbove.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellAbove.background.material.isEmptyTile)
|
||||||
val isCeiling = !isFloor && ((!cell.foreground.material.isEmptyTile && cellBelow.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellBelow.background.material.isEmptyTile))
|
val isCeiling = !isFloor && ((!cell.foreground.material.isEmptyTile && cellBelow.foreground.material.isEmptyTile) || (!cell.background.material.isEmptyTile && cellBelow.background.material.isEmptyTile))
|
||||||
|
|
||||||
@ -682,6 +683,248 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this doesn't exactly place liquids inside this chunk, it places
|
||||||
|
// them in this or any neighbouring chunk
|
||||||
|
// quite lame, since this fact makes it not thread-safe
|
||||||
|
// unless we make chunk map thread-safe
|
||||||
|
// TODO: make necessary changes for it to be thread safe?
|
||||||
|
// idea: fetch neighbour chunks and pass localized view
|
||||||
|
// of these chunks
|
||||||
|
// One big BUT: This function takes minuscule time to execute,
|
||||||
|
// is it necessary to be parallelized?
|
||||||
|
private fun generateLiquid() {
|
||||||
|
val samplePointX = pos.tileX + width / 2
|
||||||
|
val samplePointY = pos.tileY + height / 2
|
||||||
|
|
||||||
|
val blockInfo = world.template.cellInfo(samplePointX, samplePointY)
|
||||||
|
|
||||||
|
val fillLiquid = blockInfo.caveLiquid
|
||||||
|
val fillMicrodungeons = blockInfo.fillMicrodungeons
|
||||||
|
val encloseLiquids = blockInfo.encloseLiquids
|
||||||
|
|
||||||
|
// nothing to fill
|
||||||
|
if (fillLiquid.isEmptyLiquid)
|
||||||
|
return
|
||||||
|
|
||||||
|
val seedDensity = blockInfo.caveLiquidSeedDensity
|
||||||
|
|
||||||
|
if (seedDensity < 0.0)
|
||||||
|
return
|
||||||
|
|
||||||
|
// hashset instead of objectopenhashset because
|
||||||
|
// we don't care about memory footprint, only performance
|
||||||
|
var openNodes = HashSet<Vector2i>()
|
||||||
|
|
||||||
|
run {
|
||||||
|
val frequency = (100 / seedDensity).toInt()
|
||||||
|
|
||||||
|
// TODO: I don't know what to do with this code yet,
|
||||||
|
// but original sources have weird for() loop here
|
||||||
|
// (trying to do float division using integers?)
|
||||||
|
|
||||||
|
val yMin = frequency * (pos.tileY / frequency)
|
||||||
|
val xMin = frequency * (pos.tileX / frequency)
|
||||||
|
|
||||||
|
for (x in (xMin until (pos.tileX + width)).step(frequency)) {
|
||||||
|
for (y in (yMin until (pos.tileY + height)).step(frequency)) {
|
||||||
|
if (
|
||||||
|
x in pos.tileX until pos.tileX + width &&
|
||||||
|
y in pos.tileY until pos.tileY + height
|
||||||
|
) {
|
||||||
|
openNodes.add(Vector2i(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openNodes.isEmpty())
|
||||||
|
return
|
||||||
|
|
||||||
|
// Here be dragons, because I have no idea what original code
|
||||||
|
// is trying to achieve, mostly because the style it is written in
|
||||||
|
// suggests it was written WAY early in game development
|
||||||
|
|
||||||
|
var badNodes = HashSet<Vector2i>()
|
||||||
|
val candidateNodes = HashSet<Vector2i>()
|
||||||
|
|
||||||
|
// this gives a rectangle of this chunk plus all neighbours BUT the very last border cell
|
||||||
|
val bounds = AABBi(pos.tile - Vector2i(CHUNK_SIZE_FF, CHUNK_SIZE_FF), pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF))
|
||||||
|
|
||||||
|
// prohibits from opening 2nd border cell
|
||||||
|
for (x in bounds.mins.x .. bounds.maxs.x) {
|
||||||
|
badNodes.add(Vector2i(x, bounds.mins.y))
|
||||||
|
badNodes.add(Vector2i(x, bounds.maxs.y))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (y in bounds.mins.y .. bounds.maxs.y) {
|
||||||
|
badNodes.add(Vector2i(bounds.mins.x, y))
|
||||||
|
badNodes.add(Vector2i(bounds.maxs.x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun propose(position: Vector2i) {
|
||||||
|
if (!bounds.isInside(position) || position in badNodes || position in candidateNodes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cell = world.chunkMap.getCell(position)
|
||||||
|
|
||||||
|
if (cell.foreground.material.isNotEmptyTile) {
|
||||||
|
// Not sure why this doesn't poison solid materials, but it does (occasionally) encounter that case
|
||||||
|
// TODO: check if that is still the case
|
||||||
|
if (!cell.foreground.material.value.collisionKind.isSolidCollision) {
|
||||||
|
badNodes.add(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(cell.dungeonId != NO_DUNGEON_ID && (!fillMicrodungeons || cell.dungeonId != MICRO_DUNGEON_ID)) ||
|
||||||
|
(!encloseLiquids && cell.background.material.isEmptyTile) ||
|
||||||
|
(cell.liquid.state != fillLiquid && cell.liquid.state != BuiltinMetaMaterials.NO_LIQUID)
|
||||||
|
) {
|
||||||
|
badNodes.add(position)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateNodes.add(position)
|
||||||
|
openNodes.add(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
while (openNodes.isNotEmpty()) {
|
||||||
|
val oldNodes = openNodes
|
||||||
|
openNodes = HashSet()
|
||||||
|
|
||||||
|
// TODO: thats a weird choice of candidates
|
||||||
|
// maybe this can be improved?
|
||||||
|
for (node in oldNodes) {
|
||||||
|
propose(node + Vector2i(-1, 0))
|
||||||
|
propose(node + Vector2i(1, 0))
|
||||||
|
propose(node + Vector2i(0, -1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val visitedNodes = HashSet<Vector2i>()
|
||||||
|
|
||||||
|
fun poison(position: Vector2i) {
|
||||||
|
if (!bounds.isInside(position) || !visitedNodes.add(position)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cell = world.chunkMap.getCell(position)
|
||||||
|
|
||||||
|
if (cell.foreground.material.isEmptyTile) {
|
||||||
|
badNodes.add(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (badNodes.isNotEmpty()) {
|
||||||
|
val oldNodes = badNodes
|
||||||
|
badNodes = HashSet()
|
||||||
|
|
||||||
|
// TODO: same as above
|
||||||
|
for (node in oldNodes) {
|
||||||
|
candidateNodes.remove(node)
|
||||||
|
poison(node + Vector2i(-1, 0))
|
||||||
|
poison(node + Vector2i(1, 0))
|
||||||
|
poison(node + Vector2i(0, 1)) // upwards, not downwards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val solidSurroundings = HashSet(candidateNodes)
|
||||||
|
|
||||||
|
fun solids(position: Vector2i) {
|
||||||
|
val cell = world.chunkMap.getCell(position)
|
||||||
|
|
||||||
|
if (cell.foreground.material.isNotEmptyTile) {
|
||||||
|
solidSurroundings.add(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (position in candidateNodes) {
|
||||||
|
solids(position + Vector2i(1, 0))
|
||||||
|
solids(position + Vector2i(-1, 0))
|
||||||
|
solids(position + Vector2i(0, 1))
|
||||||
|
solids(position + Vector2i(0, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
val biomeBlock = blockInfo.blockBiome?.mainBlock?.native ?: BuiltinMetaMaterials.EMPTY.ref
|
||||||
|
val drops = HashMap<Vector2i, Float>()
|
||||||
|
|
||||||
|
run {
|
||||||
|
val openSet = HashSet(candidateNodes)
|
||||||
|
|
||||||
|
while (openSet.isNotEmpty()) {
|
||||||
|
val cluster = HashSet<Vector2i>()
|
||||||
|
val openCluster = HashSet<Vector2i>()
|
||||||
|
|
||||||
|
openCluster.add(openSet.first())
|
||||||
|
|
||||||
|
while (openCluster.isNotEmpty()) {
|
||||||
|
val node = openCluster.first()
|
||||||
|
openCluster.remove(node)
|
||||||
|
|
||||||
|
if (openSet.remove(node)) {
|
||||||
|
cluster.add(node)
|
||||||
|
|
||||||
|
openCluster.add(world.geometry.wrap(Vector2i(node.x, node.y + 1)))
|
||||||
|
openCluster.add(world.geometry.wrap(Vector2i(node.x, node.y - 1)))
|
||||||
|
openCluster.add(world.geometry.wrap(Vector2i(node.x + 1, node.y)))
|
||||||
|
openCluster.add(world.geometry.wrap(Vector2i(node.x - 1, node.y)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxY = Int.MIN_VALUE
|
||||||
|
var minY = Int.MAX_VALUE
|
||||||
|
|
||||||
|
for (droplet in cluster) {
|
||||||
|
if (
|
||||||
|
solidSurroundings.contains(droplet + Vector2i.POSITIVE_X) &&
|
||||||
|
solidSurroundings.contains(droplet + Vector2i.NEGATIVE_X) &&
|
||||||
|
solidSurroundings.contains(droplet + Vector2i.NEGATIVE_Y)
|
||||||
|
) {
|
||||||
|
if (droplet.y > maxY) {
|
||||||
|
maxY = droplet.y
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!solidSurroundings.contains(droplet + Vector2i.POSITIVE_Y)) {
|
||||||
|
if (droplet.y < minY) {
|
||||||
|
minY = droplet.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (droplet.y <= minY) {
|
||||||
|
minY = droplet.y - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val liquidLevel = minY.coerceAtMost(maxY)
|
||||||
|
|
||||||
|
for (node in cluster) {
|
||||||
|
val pressure = liquidLevel - node.y
|
||||||
|
|
||||||
|
if (pressure >= 0)
|
||||||
|
drops[node] = 1.0f + pressure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((position, pressure) in drops.entries) {
|
||||||
|
val cell = world.chunkMap.getCell(position).mutable()
|
||||||
|
|
||||||
|
cell.liquid.state = fillLiquid.entry!!
|
||||||
|
cell.liquid.level = 1.0f
|
||||||
|
cell.liquid.pressure = pressure
|
||||||
|
|
||||||
|
if (encloseLiquids && cell.background.material.isEmptyTile) {
|
||||||
|
cell.background.material = biomeBlock.entry ?: BuiltinMetaMaterials.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
|
world.chunkMap.setCell(position, cell.immutable())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOGGER = LogManager.getLogger()
|
private val LOGGER = LogManager.getLogger()
|
||||||
}
|
}
|
||||||
|
@ -101,37 +101,75 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
|||||||
return chunk.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
|
return chunk.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCell(position: IStruct2i) = getCell(position.component1(), position.component2())
|
||||||
|
fun setCell(position: IStruct2i, cell: AbstractCell) = setCell(position.component1(), position.component2(), cell)
|
||||||
|
|
||||||
abstract val size: Int
|
abstract val size: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// around 30% slower than ArrayChunkMap, but can support insanely large worlds
|
// around 30% slower than ArrayChunkMap, but can support insanely large worlds
|
||||||
inner class SparseChunkMap : ChunkMap() {
|
inner class SparseChunkMap : ChunkMap() {
|
||||||
private val map = Long2ObjectOpenHashMap<ChunkType>()
|
private val map = Long2ObjectOpenHashMap<ChunkType>()
|
||||||
|
// see CONCURRENT_SPARSE_CHUNK_MAP
|
||||||
|
private val lock = Any()
|
||||||
|
|
||||||
override fun get(x: Int, y: Int): ChunkType? {
|
override fun get(x: Int, y: Int): ChunkType? {
|
||||||
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
|
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
|
||||||
return map[ChunkPos.toLong(x, y)]
|
|
||||||
|
if (CONCURRENT_SPARSE_CHUNK_MAP) {
|
||||||
|
synchronized(lock) {
|
||||||
|
return map[ChunkPos.toLong(x, y)]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return map[ChunkPos.toLong(x, y)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun compute(x: Int, y: Int): ChunkType? {
|
override fun compute(x: Int, y: Int): ChunkType? {
|
||||||
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
|
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
|
||||||
val index = ChunkPos.toLong(x, y)
|
val index = ChunkPos.toLong(x, y)
|
||||||
return map[index] ?: chunkFactory(ChunkPos(x, y)).also { map[index] = it; onChunkCreated(it) }
|
|
||||||
|
if (CONCURRENT_SPARSE_CHUNK_MAP) {
|
||||||
|
synchronized(lock) {
|
||||||
|
return map[index] ?: chunkFactory(ChunkPos(x, y)).also { map[index] = it; onChunkCreated(it) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return map[index] ?: chunkFactory(ChunkPos(x, y)).also { map[index] = it; onChunkCreated(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun remove(x: Int, y: Int) {
|
override fun remove(x: Int, y: Int) {
|
||||||
val index = ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y))
|
val index = ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y))
|
||||||
val chunk = map.get(index)
|
|
||||||
|
|
||||||
if (chunk != null) {
|
if (CONCURRENT_SPARSE_CHUNK_MAP) {
|
||||||
chunk.remove()
|
synchronized(lock) {
|
||||||
onChunkRemoved(chunk)
|
val chunk = map.get(index)
|
||||||
map.remove(index)
|
|
||||||
|
if (chunk != null) {
|
||||||
|
chunk.remove()
|
||||||
|
onChunkRemoved(chunk)
|
||||||
|
map.remove(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val chunk = map.get(index)
|
||||||
|
|
||||||
|
if (chunk != null) {
|
||||||
|
chunk.remove()
|
||||||
|
onChunkRemoved(chunk)
|
||||||
|
map.remove(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chunks(): List<ChunkType> {
|
override fun chunks(): List<ChunkType> {
|
||||||
return ObjectArrayList(map.values)
|
if (CONCURRENT_SPARSE_CHUNK_MAP) {
|
||||||
|
synchronized(lock) {
|
||||||
|
return ObjectArrayList(map.values)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ObjectArrayList(map.values)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val size: Int
|
override val size: Int
|
||||||
@ -341,5 +379,23 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOGGER = LogManager.getLogger()
|
private val LOGGER = LogManager.getLogger()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Employ locking since concurrent reads can
|
||||||
|
* cause random exceptions in event of concurrent write
|
||||||
|
* (due to hash table resize)
|
||||||
|
*
|
||||||
|
* This obviously seriously impacts performance,
|
||||||
|
* but there is no other way unless we sacrifice concurrent
|
||||||
|
* chunk generation
|
||||||
|
*
|
||||||
|
* On other hand, currently, terrain is the real performance hog
|
||||||
|
* of world generation, everything else is kinda okay to be performed
|
||||||
|
* on main world thread, so concurrent access is not needed for now.
|
||||||
|
*
|
||||||
|
* ArrayChunkMap does not need synchronization since concurrent write
|
||||||
|
* won't cause any observable side effects by concurrent readers.
|
||||||
|
*/
|
||||||
|
private const val CONCURRENT_SPARSE_CHUNK_MAP = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user