From 43e9c2412d39c8067b34f07c758bba41b615b776 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 6 Apr 2024 01:57:44 +0700 Subject: [PATCH] Cave liquid --- .../defs/tile/BuiltinMetaMaterials.kt | 3 + .../kstarbound/defs/world/WorldLayout.kt | 6 +- .../kstarbound/defs/world/WorldTemplate.kt | 10 +- .../kstarbound/server/world/ServerChunk.kt | 259 +++++++++++++++++- .../ru/dbotthepony/kstarbound/world/World.kt | 72 ++++- 5 files changed, 331 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index 288498e6..f648f5be 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -26,6 +26,9 @@ val Registry.Ref.isObjectPlatformTile: Boolean val Registry.Entry.isEmptyTile: Boolean get() = this == BuiltinMetaMaterials.EMPTY || this == BuiltinMetaMaterials.NULL +val Registry.Entry.isNotEmptyTile: Boolean + get() = !isEmptyTile + val Registry.Entry.isNullTile: Boolean get() = this == BuiltinMetaMaterials.NULL diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index 7fe13885..72c1ce58 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -553,9 +553,11 @@ class WorldLayout { val yi: Int - if (y < layers.first().yStart) { + if (y == layers.first().yStart) { + yi = 0 + } else if (y < layers.first().yStart) { return emptyList() - } else if (y > layers.last().yStart) { + } else if (y >= layers.last().yStart) { yi = layers.size } else { yi = layers.indexOfFirst { it.yStart >= y } - 1 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 31e0f76f..a1978697 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -204,8 +204,16 @@ class WorldTemplate(val geometry: WorldGeometry) { 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() - .maximumSize(125_000L) + .maximumSize(1_000_000L) .expireAfterAccess(Duration.ofMinutes(2)) .executor(Starbound.EXECUTOR) .scheduler(Scheduler.systemScheduler()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 1f9b11d1..a1033f06 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -4,27 +4,31 @@ import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArraySet import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.arrays.Object2DArray +import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound 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.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult 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.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNullTile import ru.dbotthepony.kstarbound.defs.tile.supportsModifier import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.util.random.staticRandomDouble 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.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener @@ -132,11 +136,13 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { - //LOGGER.error("NYI: Generating cave liquids for $chunk") + // not thread safe, but takes very little time to execute + generateLiquid() } State.FULL -> { - CompletableFuture.runAsync(Runnable { placeGrass() }, Starbound.EXECUTOR).await() + // CompletableFuture.runAsync(Runnable { placeGrass() }, Starbound.EXECUTOR).await() + placeGrass() } State.FRESH -> throw RuntimeException() @@ -632,11 +638,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk() + + 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() + val candidateNodes = HashSet() + + // 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() + + 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() + + run { + val openSet = HashSet(candidateNodes) + + while (openSet.isNotEmpty()) { + val cluster = HashSet() + val openCluster = HashSet() + + 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 { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 880e93a1..21764ce0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -101,37 +101,75 @@ abstract class World, ChunkType : Chunk() + // see CONCURRENT_SPARSE_CHUNK_MAP + private val lock = Any() override fun get(x: Int, y: Int): ChunkType? { 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? { if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null 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) { val index = ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y)) - val chunk = map.get(index) - if (chunk != null) { - chunk.remove() - onChunkRemoved(chunk) - map.remove(index) + if (CONCURRENT_SPARSE_CHUNK_MAP) { + synchronized(lock) { + val chunk = map.get(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 { - return ObjectArrayList(map.values) + if (CONCURRENT_SPARSE_CHUNK_MAP) { + synchronized(lock) { + return ObjectArrayList(map.values) + } + } else { + return ObjectArrayList(map.values) + } } override val size: Int @@ -341,5 +379,23 @@ abstract class World, ChunkType : Chunk