Cave liquid

This commit is contained in:
DBotThePony 2024-04-06 01:57:44 +07:00
parent 6f6150475e
commit 43e9c2412d
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 331 additions and 19 deletions

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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()
} }

View File

@ -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
} }
} }