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 67f38429..288498e6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -24,7 +24,7 @@ val Registry.Ref.isObjectPlatformTile: Boolean get() = entry == BuiltinMetaMaterials.OBJECT_PLATFORM val Registry.Entry.isEmptyTile: Boolean - get() = this == BuiltinMetaMaterials.EMPTY + get() = this == BuiltinMetaMaterials.EMPTY || this == BuiltinMetaMaterials.NULL val Registry.Entry.isNullTile: Boolean get() = this == BuiltinMetaMaterials.NULL @@ -52,6 +52,21 @@ val Registry.Entry.isEmptyLiquid: Boolean val Registry.Ref.isEmptyLiquid: Boolean get() = entry == null || entry == BuiltinMetaMaterials.NO_LIQUID +// these are hardcoded way harder than any Hard-Coder:tm: +// considering there is no way you gonna mod-in this many (16 bit uint) dungeons +const val NO_DUNGEON_ID = 65535 +const val SPAWN_DUNGEON_ID = 65534 +const val MICRO_DUNGEON_ID = 65533 +// meta dungeon signalling player built structures +const val ARTIFICIAL_DUNGEON_ID = 65532 +// indicates a block that has been destroyed +const val DESTROYED_BLOCK_ID = 65531 +// dungeonId for zero-g areas with and without tile protection +const val ZERO_GRAVITY_DUNGEON_ID = 65525 +const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524 + +const val FIRST_RESERVED_DUNGEON_ID = 65520 + object BuiltinMetaMaterials { private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition( materialId = id, @@ -117,7 +132,7 @@ object BuiltinMetaMaterials { OBJECT_PLATFORM, ) - val EMPTY_MOD = makeMod(65535, "empty") + val EMPTY_MOD = makeMod(65535, "none") val BIOME_MOD = makeMod(65534, "biome") val UNDERGROUND_BIOME_MOD = makeMod(65533, "underground_biome") 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 fe35f891..31e0f76f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -1,32 +1,27 @@ package ru.dbotthepony.kstarbound.defs.world +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Scheduler import com.google.gson.JsonObject -import ru.dbotthepony.kommons.arrays.Object2DArray -import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Globals -import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition -import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile -import ru.dbotthepony.kstarbound.defs.tile.isNullTile -import ru.dbotthepony.kstarbound.defs.tile.supportsModifier import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.math.quintic2 import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.WorldGeometry -import ru.dbotthepony.kstarbound.world.api.ImmutableCell -import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.physics.Poly +import java.time.Duration import java.util.random.RandomGenerator class WorldTemplate(val geometry: WorldGeometry) { @@ -156,6 +151,38 @@ class WorldTemplate(val geometry: WorldGeometry) { return geometry.size.y / 2 } + data class PotentialBiomeItems( + // Potential items that would spawn at the given block assuming it is at + val surfaceBiomeItems: List, + + // ... Or on a cave surface. + val caveSurfaceBiomeItems: List, + + // ... Or on a cave ceiling. + val caveCeilingBiomeItems: List, + + // ... Or on a cave background wall. + val caveBackgroundBiomeItems: List, + + // ... Or in the ocean + val oceanItems: List, + ) + + fun potentialBiomeItemsAt(x: Int, y: Int): PotentialBiomeItems { + val thisBlockBiome = cellInfo(geometry.x.cell(x), geometry.y.cell(y)).blockBiome + val lowerBlockBiome = cellInfo(geometry.x.cell(x), geometry.y.cell(y - 1)).blockBiome + val upperBlockBiome = cellInfo(geometry.x.cell(x), geometry.y.cell(y + 1)).blockBiome + + return PotentialBiomeItems( + surfaceBiomeItems = lowerBlockBiome?.surfacePlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR } ?: listOf(), + oceanItems = thisBlockBiome?.surfacePlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.OCEAN } ?: listOf(), + + caveSurfaceBiomeItems = lowerBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.FLOOR } ?: listOf(), + caveCeilingBiomeItems = upperBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.CEILING } ?: listOf(), + caveBackgroundBiomeItems = thisBlockBiome?.undergroundPlaceables?.itemDistributions?.filter { it.mode == BiomePlaceablesDefinition.Placement.BACKGROUND } ?: listOf(), + ) + } + class CellInfo(val x: Int, val y: Int) { var foreground: Registry.Ref = BuiltinMetaMaterials.EMPTY.ref var foregroundMod: Registry.Ref = BuiltinMetaMaterials.EMPTY_MOD.ref @@ -177,7 +204,18 @@ class WorldTemplate(val geometry: WorldGeometry) { var backgroundCave = false } + private val cellCache = Caffeine.newBuilder() + .maximumSize(125_000L) + .expireAfterAccess(Duration.ofMinutes(2)) + .executor(Starbound.EXECUTOR) + .scheduler(Scheduler.systemScheduler()) + .build { (x, y) -> cellInfo0(x, y) } + fun cellInfo(x: Int, y: Int): CellInfo { + return cellCache.get(Vector2i(x, y)) + } + + private fun cellInfo0(x: Int, y: Int): CellInfo { val info = CellInfo(x, y) val layout = worldLayout ?: return info diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index ab37d10d..77c76549 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -54,7 +54,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn // packets which interact with world must be // executed on world's thread fun enqueue(task: ServerWorld.() -> Unit) { - return tracker?.enqueue(task) ?: throw IllegalStateException("Not in world.") + return tracker?.enqueue(task) ?: LOGGER.warn("$this tried to interact with world, but they are not in one.") } lateinit var shipWorld: ServerWorld 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 70638958..1f9b11d1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -4,6 +4,8 @@ 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 @@ -12,6 +14,7 @@ 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.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageType @@ -20,6 +23,7 @@ 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 import ru.dbotthepony.kstarbound.world.ChunkPos @@ -33,6 +37,7 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.locks.ReentrantLock import java.util.function.Predicate import kotlin.concurrent.withLock +import kotlin.coroutines.cancellation.CancellationException class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(world, pos) { /** @@ -46,9 +51,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk= State.MICRO_DUNGEONS) { + val neighbours = ArrayList() + + try { + // wait for neighbouring chunks to be as generated as our current state + // before we advance to next stage + // Yes, this will create quite big loading rectangle around + // players and other watching entities, but this is required + // to avoid generation artifacts, where this chunk tries to read + // from not sufficiently generated neighbours + + for (neighbour in pos.neighbours()) { + val ticket = world.permanentChunkTicket(neighbour, state) ?: continue + neighbours.add(ticket) + } + + for (neighbour in neighbours) { + var i = 0 + + while (!neighbour.chunk.isDone && ++i < 20) { + delay(500L) + } + + if (!neighbour.chunk.isDone) { + LOGGER.error("Giving up waiting on ${neighbour.pos} while advancing generation stage of $this to $nextState (neighbour chunk was in state ${world.chunkMap[neighbour.pos]?.state}, expected $state)") + } + } + } catch (err: Throwable) { + LOGGER.error("Error while waiting on neighbouring chunks while generating this $this", err) + throw err + } finally { + neighbours.forEach { it.cancel() } + } + } + when (nextState) { State.TILES -> { // tiles can be generated concurrently without any consequences @@ -98,15 +135,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { - //LOGGER.error("NYI: Generating tile entities for $chunk") + State.FULL -> { + CompletableFuture.runAsync(Runnable { placeGrass() }, Starbound.EXECUTOR).await() } - State.ENTITIES -> { - //LOGGER.error("NYI: Placing entities for $chunk") - } - - else -> {} + State.FRESH -> throw RuntimeException() } bumpState(nextState) @@ -145,7 +178,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk= targetState) { chunk.complete(this@ServerChunk) - } else { - this@ServerChunk.targetState.trySend(targetState) } } @@ -248,6 +283,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk= state) { "Tried to downgrade $this state from $state to $newState" } - - if (newState >= State.ENTITIES) { - this.state = State.FULL - } else { - this.state = newState - } + this.state = newState val permanent: List val temporary: List @@ -432,7 +464,9 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk 600 + // also make partially-generated chunks stay in memory for way longer, because re-generating + // them is very costly operation + shouldUnload = if (state == State.FULL) idleTicks > 600 else idleTicks > 32000 } else { idleTicks = 0 } @@ -507,7 +541,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { return ChunkPos(x + 1, y) } + fun neighbours(): List { + return listOf(top, bottom, left, right, topLeft, topRight, bottomLeft, bottomRight) + } + override fun equals(other: Any?): Boolean { if (other is ChunkPos) return other.x == x && other.y == y diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt index a4eb403c..8d9bb25d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api import com.github.benmanes.caffeine.cache.Interner import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import java.io.DataInputStream import java.io.DataOutputStream @@ -11,7 +12,7 @@ sealed class AbstractCell { abstract val background: AbstractTileState abstract val liquid: AbstractLiquidState - // ushort, can be anything (but 65535), mostly used by placed dungeons + // ushort, can be anything, mostly used by placed dungeons // this value determines special logic behind this cell, such as if it is protected from modifications // by players or other means abstract val dungeonId: Int @@ -63,7 +64,7 @@ sealed class AbstractCell { @JvmStatic protected val POOL: Interner = if (Starbound.DEDUP_CELL_STATES) Starbound.interner() else Interner { it } - val EMPTY: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.EMPTY, AbstractTileState.EMPTY, AbstractLiquidState.EMPTY, 0, 0, 0, false)) - val NULL: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.NULL, AbstractTileState.NULL, AbstractLiquidState.EMPTY, 0, 0, 0, false)) + val EMPTY: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.EMPTY, AbstractTileState.EMPTY, AbstractLiquidState.EMPTY, NO_DUNGEON_ID, 0, 0, false)) + val NULL: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.NULL, AbstractTileState.NULL, AbstractLiquidState.EMPTY, NO_DUNGEON_ID, 0, 0, false)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt index bb631637..82b3a8c0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world.api +import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState data class ImmutableCell( @@ -7,7 +8,7 @@ data class ImmutableCell( override val background: ImmutableTileState = AbstractTileState.EMPTY, override val liquid: ImmutableLiquidState = AbstractLiquidState.EMPTY, - override val dungeonId: Int = 0, + override val dungeonId: Int = NO_DUNGEON_ID, override val blockBiome: Int = 0, override val envBiome: Int = 0, override val isIndestructible: Boolean = false, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt index 3ca47a36..e77a9328 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableTileState.kt @@ -7,11 +7,11 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState data class ImmutableTileState( - override var material: Registry.Entry = BuiltinMetaMaterials.NULL, - override var modifier: Registry.Entry = BuiltinMetaMaterials.EMPTY_MOD, - override var color: TileColor = TileColor.DEFAULT, - override var hueShift: Float = 0f, - override var modifierHueShift: Float = 0f, + override val material: Registry.Entry = BuiltinMetaMaterials.NULL, + override val modifier: Registry.Entry = BuiltinMetaMaterials.EMPTY_MOD, + override val color: TileColor = TileColor.DEFAULT, + override val hueShift: Float = 0f, + override val modifierHueShift: Float = 0f, ) : AbstractTileState() { override fun immutable(): ImmutableTileState { return this diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt index 4a323279..29e6a329 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world.api +import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import java.io.DataInputStream data class MutableCell( @@ -7,7 +8,7 @@ data class MutableCell( override val background: MutableTileState = MutableTileState(), override val liquid: MutableLiquidState = MutableLiquidState(), - override var dungeonId: Int = 0, + override var dungeonId: Int = NO_DUNGEON_ID, override var blockBiome: Int = 0, override var envBiome: Int = 0, override var isIndestructible: Boolean = false,