More work on objects, orientations and spaces are working? kind of

This commit is contained in:
DBotThePony 2024-04-09 10:56:50 +07:00
parent c2e5b32c94
commit c91b448e66
Signed by: DBot
GPG Key ID: DCC23B5715498507
55 changed files with 1089 additions and 388 deletions

View File

@ -52,3 +52,12 @@ val color: TileColor = TileColor.DEFAULT
### player.config ### player.config
* Inventory bags are no longer limited to 255 slots * Inventory bags are no longer limited to 255 slots
* However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too) * However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too)
---------------
### Prototypes
#### .matierial
* Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`)
* Used by object and plant anchoring code to determine valid placement
* Used by world tile rendering code (render piece rule `Connects`)

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10 kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.12.3 kommonsVersion=2.13.1
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -106,7 +106,8 @@ object Registries {
private inline fun <reified T : Any> loadRegistry( private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>, registry: Registry<T>,
files: List<IStarboundFile>, files: List<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, Int?> noinline keyProvider: (T) -> Pair<String, Int?>,
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
): List<Future<*>> { ): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) } val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) }
@ -120,6 +121,8 @@ object Registries {
val read = adapter.fromJsonTree(elem) val read = adapter.fromJsonTree(elem)
val keys = keyProvider(read) val keys = keyProvider(read)
after(read, listedFile)
registry.add { registry.add {
if (keys.second != null) if (keys.second != null)
registry.add(keys.first, keys.second!!, read, elem, listedFile) registry.add(keys.first, keys.second!!, read, elem, listedFile)

View File

@ -5,7 +5,6 @@ import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.* import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.AABBTypeAdapter import ru.dbotthepony.kommons.gson.AABBTypeAdapter
@ -59,11 +58,10 @@ import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory
import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.JsonPath
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutorWithScheduler
import ru.dbotthepony.kstarbound.util.Directives import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.* import java.io.*
@ -138,7 +136,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
@JvmField @JvmField
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool() val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
@JvmField @JvmField
val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher() val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher()
@JvmField @JvmField
val CLEANER: Cleaner = Cleaner.create { val CLEANER: Cleaner = Cleaner.create {
@ -152,7 +150,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
// Hrm. // Hrm.
// val strings: Interner<String> = Interner.newWeakInterner() // val strings: Interner<String> = Interner.newWeakInterner()
// val strings: Interner<String> = Interner { it } // val strings: Interner<String> = Interner { it }
@JvmField @JvmField
val STRINGS: Interner<String> = interner(5) val STRINGS: Interner<String> = interner(5)
// immeasurably lazy and fragile solution, too bad! // immeasurably lazy and fragile solution, too bad!

View File

@ -62,6 +62,7 @@ import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity import ru.dbotthepony.kstarbound.math.roundTowardsPositiveInfinity
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.formatBytesShort import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayDirection import ru.dbotthepony.kstarbound.world.RayDirection
import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.LightCalculator
@ -548,8 +549,8 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo
return world!!.getCell(x + viewportCellX, y + viewportCellY) return world!!.getCell(x + viewportCellX, y + viewportCellY)
} }
override fun getCellDirect(x: Int, y: Int): AbstractCell { override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
return world!!.getCellDirect(x + viewportCellX, y + viewportCellY) return world!!.setCell(x + viewportCellX, y + viewportCellY, cell, chunkState)
} }
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {

View File

@ -2,9 +2,19 @@ package ru.dbotthepony.kstarbound.client.world
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.ChunkState
import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos){ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos) {
override val state: ChunkState
get() = ChunkState.FULL
// we don't care about current status since we are always considered to be fully loaded
override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
return setCell(x, y, cell)
}
override fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) { override fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) {
super.foregroundChanges(x, y, cell) super.foregroundChanges(x, y, cell)

View File

@ -47,7 +47,7 @@ class ClientWorld(
throw RuntimeException("unreachable code") throw RuntimeException("unreachable code")
} }
override val isRemote: Boolean override val isClient: Boolean
get() = true get() = true
val renderRegionWidth = determineChunkSize(geometry.size.x) val renderRegionWidth = determineChunkSize(geometry.size.x)

View File

@ -33,7 +33,7 @@ data class UniverseServerConfig(
if (floatingDungeon != null) { if (floatingDungeon != null) {
if (t !is FloatingDungeonWorldParameters) return false if (t !is FloatingDungeonWorldParameters) return false
if (t.primaryDungeon != floatingDungeon) return false if (t.primaryDungeon.key.left() != floatingDungeon) return false
} }
return true return true

View File

@ -7,4 +7,5 @@ class WorldServerConfig(
val playerStartRegionMaximumTries: Int = 1, val playerStartRegionMaximumTries: Int = 1,
val playerStartRegionMaximumVerticalSearch: Int = 1, val playerStartRegionMaximumVerticalSearch: Int = 1,
val playerStartRegionSize: Vector2d, val playerStartRegionSize: Vector2d,
val spawnDungeonRetries: Int = 1,
) )

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -16,6 +17,7 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@ -41,7 +43,7 @@ data class DungeonDefinition(
val metadata: Metadata, val metadata: Metadata,
// relevant for PNG defined dungeons // relevant for PNG defined dungeons
val tiles: ImageTileSet = ImageTileSet(), val tiles: ImageTileSet = ImageTileSet(),
val parts: ImmutableList<DungeonPart>, private val parts: ImmutableList<JsonObject>,
) { ) {
@JsonFactory @JsonFactory
data class Metadata( data class Metadata(
@ -67,22 +69,39 @@ data class DungeonDefinition(
get() = metadata.name get() = metadata.name
init { init {
parts.forEach { it.bind(this) } tiles.spewWarnings(name)
}
for (anchor in metadata.anchor) { private val directory = AssetPathStack.last()
if (!parts.any { it.name == anchor }) {
throw JsonSyntaxException("Dungeon contains $anchor as anchor, but there is no such part") val actualParts: ImmutableList<DungeonPart> by lazy {
AssetPathStack(directory) {
val build = parts.stream().map { Starbound.gson.fromJson(it, DungeonPart::class.java) }.collect(ImmutableList.toImmutableList())
build.forEach { it.bind(this) }
for (anchor in metadata.anchor) {
if (!build.any { it.name == anchor }) {
throw JsonSyntaxException("Dungeon contains $anchor as anchor, but there is no such part")
}
} }
build
} }
} }
val partMap: ImmutableMap<String, DungeonPart> = parts.stream().map { it.name to it }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
val anchorParts: ImmutableList<DungeonPart> = metadata.anchor.stream().map { anchor -> parts.first { it.name == anchor } }.collect(ImmutableList.toImmutableList()) val partMap: ImmutableMap<String, DungeonPart> by lazy {
actualParts.stream().map { it.name to it }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
}
val anchorParts: ImmutableList<DungeonPart> by lazy {
metadata.anchor.stream().map { anchor -> actualParts.first { it.name == anchor } }.collect(ImmutableList.toImmutableList())
}
private fun connectableParts(connector: DungeonPart.JigsawConnector): List<DungeonPart.JigsawConnector> { private fun connectableParts(connector: DungeonPart.JigsawConnector): List<DungeonPart.JigsawConnector> {
val result = ArrayList<DungeonPart.JigsawConnector>() val result = ArrayList<DungeonPart.JigsawConnector>()
for (part in parts) { for (part in actualParts) {
if (!part.doesNotConnectTo(connector.part)) { if (!part.doesNotConnectTo(connector.part)) {
for (pconnector in part.connectors) { for (pconnector in part.connectors) {
if (pconnector.connectsTo(connector)) { if (pconnector.connectsTo(connector)) {
@ -183,7 +202,7 @@ data class DungeonDefinition(
} }
} }
world.pressurizeLiquids() world.applyFinalTouches()
} }
fun generate(world: ServerWorld, random: RandomGenerator, x: Int, y: Int, markSurfaceAndTerrain: Boolean, forcePlacement: Boolean, dungeonID: Int = 0, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture<DungeonWorld> { fun generate(world: ServerWorld, random: RandomGenerator, x: Int, y: Int, markSurfaceAndTerrain: Boolean, forcePlacement: Boolean, dungeonID: Int = 0, terrainSurfaceSpaceExtends: Int = 0, commit: Boolean = true): CompletableFuture<DungeonWorld> {
@ -201,7 +220,7 @@ data class DungeonDefinition(
return CoroutineScope(Starbound.COROUTINE_EXECUTOR) return CoroutineScope(Starbound.COROUTINE_EXECUTOR)
.async { .async {
if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, world)) { if (forcePlacement || anchor.canPlace(x, y - anchor.placementLevelConstraint, dungeonWorld)) {
generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID) generate0(anchor, dungeonWorld, x, y - anchor.placementLevelConstraint, forcePlacement, dungeonID)
if (commit) { if (commit) {

View File

@ -189,7 +189,7 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null) if (world.markSurfaceLevel != null)
return y < world.markSurfaceLevel return y < world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y) val cell = world.parent.getCell(x, y)
if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)) if (cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y))
return false return false
@ -198,7 +198,7 @@ abstract class DungeonRule {
} }
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y) val cell = world.getCell(x, y)
return cell.foreground.material.isNotEmptyTile return cell.foreground.material.isNotEmptyTile
} }
@ -215,12 +215,12 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null) if (world.markSurfaceLevel != null)
return y >= world.markSurfaceLevel return y >= world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y) val cell = world.parent.getCell(x, y)
return cell.foreground.material.isEmptyTile || cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y) return cell.foreground.material.isEmptyTile || cell.foreground.material.isObjectTile && world.isClearingTileEntityAt(x, y)
} }
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y) val cell = world.getCell(x, y)
return cell.foreground.material.isEmptyTile return cell.foreground.material.isEmptyTile
} }
@ -237,7 +237,7 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null) if (world.markSurfaceLevel != null)
return y < world.markSurfaceLevel return y < world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y) val cell = world.parent.getCell(x, y)
if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)) if (cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y))
return false return false
@ -246,7 +246,7 @@ abstract class DungeonRule {
} }
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y) val cell = world.getCell(x, y)
return cell.background.material.isNotEmptyTile return cell.background.material.isNotEmptyTile
} }
@ -263,12 +263,12 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null) if (world.markSurfaceLevel != null)
return y >= world.markSurfaceLevel return y >= world.markSurfaceLevel
val cell = world.parent.chunkMap.getCell(x, y) val cell = world.parent.getCell(x, y)
return cell.background.material.isEmptyTile || cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y) return cell.background.material.isEmptyTile || cell.background.material.isObjectTile && world.isClearingTileEntityAt(x, y)
} }
override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean { override fun checkTileCanPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y) val cell = world.getCell(x, y)
return cell.background.material.isEmptyTile return cell.background.material.isEmptyTile
} }

View File

@ -69,7 +69,7 @@ data class DungeonTile(
// but thats also not a priority, since this check happens quite quickly // but thats also not a priority, since this check happens quite quickly
// to have any noticeable impact on world's performance // to have any noticeable impact on world's performance
fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean { fun canPlace(x: Int, y: Int, world: DungeonWorld): Boolean {
val cell = world.parent.chunkMap.getCell(x, y) val cell = world.parent.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID) if (cell.dungeonId != NO_DUNGEON_ID)
return false return false
@ -81,7 +81,7 @@ data class DungeonTile(
} }
fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean { fun canPlace(x: Int, y: Int, world: ServerWorld): Boolean {
val cell = world.chunkMap.getCell(x, y) val cell = world.getCell(x, y)
if (cell.dungeonId != NO_DUNGEON_ID) if (cell.dungeonId != NO_DUNGEON_ID)
return false return false

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi 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
@ -16,16 +17,20 @@ import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.server.world.ServerChunk import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.MutableTileState import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.Collections import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer import java.util.function.Consumer
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@ -63,18 +68,28 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
val parameters: JsonObject = JsonObject() val parameters: JsonObject = JsonObject()
) )
private val liquid = HashMap<Vector2i, AbstractLiquidState>() var hasGenerated = false
private val foregroundMaterial = HashMap<Vector2i, Material>() private set
private val foregroundModifier = HashMap<Vector2i, Modifier>()
private val backgroundMaterial = HashMap<Vector2i, Material>() val targetChunkState = if (parent.template.worldParameters is FloatingDungeonWorldParameters) ChunkState.FULL else ChunkState.TERRAIN
private val backgroundModifier = HashMap<Vector2i, Modifier>()
private val liquid = HashMap<Vector2i, AbstractLiquidState>(8192, 0.5f)
private val foregroundMaterial = HashMap<Vector2i, Material>(8192, 0.5f)
private val foregroundModifier = HashMap<Vector2i, Modifier>(8192, 0.5f)
private val backgroundMaterial = HashMap<Vector2i, Material>(8192, 0.5f)
private val backgroundModifier = HashMap<Vector2i, Modifier>(8192, 0.5f)
// for entity spaces which should be considered empty if they // for entity spaces which should be considered empty if they
// are occupied by tile entity // are occupied by tile entity
private val clearTileEntitiesAt = HashSet<Vector2i>() private val clearTileEntitiesAt = HashSet<Vector2i>(8192, 0.5f)
// entities themselves to be removed // entities themselves to be removed
private val tileEntitiesToRemove = HashSet<TileEntity>() private val tileEntitiesToRemove = HashSet<TileEntity>(2048, 0.5f)
private val touchedTiles = HashSet<Vector2i>(16384, 0.5f)
private val protectTile = HashSet<Vector2i>(16384, 0.5f)
private val boundingBoxes = ArrayList<AABBi>()
fun clearTileEntityAt(x: Int, y: Int) { fun clearTileEntityAt(x: Int, y: Int) {
clearTileEntitiesAt.add(geometry.wrap(Vector2i(x, y))) clearTileEntitiesAt.add(geometry.wrap(Vector2i(x, y)))
@ -92,11 +107,6 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
tileEntitiesToRemove.add(entity) tileEntitiesToRemove.add(entity)
} }
private val touchedTiles = HashSet<Vector2i>()
private val protectTile = HashSet<Vector2i>()
private val boundingBoxes = ArrayList<AABBi>()
private var currentBoundingBox: AABBi? = null private var currentBoundingBox: AABBi? = null
fun touched(): Set<Vector2i> = Collections.unmodifiableSet(touchedTiles) fun touched(): Set<Vector2i> = Collections.unmodifiableSet(touchedTiles)
@ -128,15 +138,15 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
return protectTile.contains(geometry.wrap(Vector2i(x, y))) return protectTile.contains(geometry.wrap(Vector2i(x, y)))
} }
private val biomeItems = HashSet<Vector2i>() private val biomeItems = HashSet<Vector2i>(8192, 0.5f)
private val biomeTrees = HashSet<Vector2i>() private val biomeTrees = HashSet<Vector2i>()
private val itemDrops = HashMap<Vector2i, ArrayList<ItemDescriptor>>() private val itemDrops = HashMap<Vector2i, ArrayList<ItemDescriptor>>()
private val randomizedItemDrops = HashMap<Vector2i, ArrayList<ItemDescriptor>>() private val randomizedItemDrops = HashMap<Vector2i, ArrayList<ItemDescriptor>>()
private val dungeonIDs = HashMap<Vector2i, Int>() private val dungeonIDs = HashMap<Vector2i, Int>(16384, 0.5f)
private var dungeonID = -1 private var dungeonID = -1
private val pendingLiquids = HashMap<Vector2i, AbstractLiquidState>() private val pendingLiquids = HashMap<Vector2i, AbstractLiquidState>(8192, 0.5f)
private val openLocalWires = HashMap<String, HashSet<Vector2i>>() private val openLocalWires = HashMap<String, HashSet<Vector2i>>()
private val globalWires = HashMap<String, HashSet<Vector2i>>() private val globalWires = HashMap<String, HashSet<Vector2i>>()
@ -268,7 +278,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
val tickets = ArrayList<ServerChunk.ITicket>() val tickets = ArrayList<ServerChunk.ITicket>()
return try { return try {
tickets.addAll(parent.permanentChunkTicket(region, ServerChunk.State.TERRAIN)) tickets.addAll(parent.permanentChunkTicket(region, targetChunkState))
tickets.forEach { it.chunk.await() } tickets.forEach { it.chunk.await() }
block() block()
} finally { } finally {
@ -290,7 +300,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
return waitForRegionAndJoin(AABBi(position, position + size), block) return waitForRegionAndJoin(AABBi(position, position + size), block)
} }
fun pressurizeLiquids() { fun applyFinalTouches() {
// For each liquid type, find each contiguous region of liquid, then // For each liquid type, find each contiguous region of liquid, then
// pressurize that region based on the highest position in the region // pressurize that region based on the highest position in the region
@ -349,6 +359,8 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
} }
pendingLiquids.clear() pendingLiquids.clear()
hasGenerated = true
} }
private fun applyCellChangesAt(pos: Vector2i, chunk: ServerChunk) { private fun applyCellChangesAt(pos: Vector2i, chunk: ServerChunk) {
@ -422,64 +434,95 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
}.await() }.await()
for (box in boundingBoxes) { for (box in boundingBoxes) {
tickets.addAll(parent.permanentChunkTicket(box, ServerChunk.State.TERRAIN)) tickets.addAll(parent.permanentChunkTicket(box, targetChunkState))
} }
// apply tiles to world per-chunk // apply tiles to world per-chunk
// this way we don't need to wait on all chunks to be loaded // this way we don't need to wait on all chunks to be loaded
// and apply changes chunks which have been loaded right away // and apply changes chunks which have been loaded right away
val tilePositionsRaw = ArrayList<Vector2i>() val tilePositions = HashSet<Vector2i>(
foregroundMaterial.keys.size
.coerceAtLeast(foregroundModifier.keys.size)
.coerceAtLeast(backgroundMaterial.keys.size)
.coerceAtLeast(backgroundModifier.keys.size)
.coerceAtLeast(liquid.keys.size)
)
tilePositionsRaw.addAll(foregroundMaterial.keys) tilePositions.addAll(foregroundMaterial.keys)
tilePositionsRaw.addAll(foregroundModifier.keys) tilePositions.addAll(foregroundModifier.keys)
tilePositionsRaw.addAll(backgroundMaterial.keys) tilePositions.addAll(backgroundMaterial.keys)
tilePositionsRaw.addAll(backgroundModifier.keys) tilePositions.addAll(backgroundModifier.keys)
tilePositions.addAll(liquid.keys)
tilePositionsRaw.sortWith { o1, o2 ->
val cmp = o1.x.compareTo(o2.x)
if (cmp == 0) o1.y.compareTo(o2.y) else cmp
}
val regions = Long2ObjectOpenHashMap<ArrayList<Vector2i>>() val regions = Long2ObjectOpenHashMap<ArrayList<Vector2i>>()
var previous: Vector2i? = null
for (pos in tilePositionsRaw) { for (pos in tilePositions) {
if (pos != previous) { regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos)
regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos)
previous = pos
}
} }
val seenTickets = HashSet<ChunkPos>() val seenTickets = HashSet<ChunkPos>()
val waiters = ArrayList<CompletableFuture<*>>()
for (ticket in tickets.filter { seenTickets.add(it.pos) }) { for (ticket in tickets.filter { seenTickets.add(it.pos) }) {
// make changes to chunk only inside world's thread once it has reached TILES state // make changes to chunk only inside world's thread once it has reached TILES state
ticket.chunk.thenAcceptAsync(Consumer { waiters.add(ticket.chunk.thenAcceptAsync(Consumer {
regions.get(ticket.pos.toLong())?.forEach { applyCellChangesAt(it, ticket.chunk.get()) } regions.get(ticket.pos.toLong())?.forEach {
}, parent.eventLoop) applyCellChangesAt(it, ticket.chunk.get())
}
}, parent.eventLoop))
} }
// wait for all chunks to be loaded // wait for all chunks to be loaded (and cell changes to be applied)
tickets.forEach { it.chunk.await() } // if any of cell change operation fails, entire generation fails... leaving world in inconsistent state,
// but also limiting damage by exiting early.
waiters.forEach { it.await() }
// at this point all chunks are available, and we applied changes to tiles // at this point all chunks are available, and we applied changes to tiles
if (playerStart != null)
parent.setPlayerSpawn(playerStart!!, false)
// and finally, schedule chunks to be loaded into FULL state val placedObjects = placedObjects.entries.stream()
// this way, big dungeons won't get cut off when chunks being saved .sorted { o1, o2 -> o1.key.y.compareTo(o2.key.y) } // place objects from bottom to top
// to disk because of multiple chunks outside player tracking area // so objects stacked on each other can be properly placed
// But this might trigger cascading world generation .map { (pos, data) ->
// (big dungeon generates another big dungeon, and another, and so on), WorldObject.create(data.prototype, pos, data.parameters) to data.direction
// tough, so need to take care! }
for (box in boundingBoxes) { .filter { it.first != null }
// specify timer as 0 so ticket gets removed on next world tick .toList()
parent.temporaryChunkTicket(box, 0, ServerChunk.State.FULL)
parent.eventLoop.supplyAsync {
for ((obj, direction) in placedObjects) {
val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction)
if (orientation != -1) {
obj.setOrientation(orientation)
obj.joinWorld(parent)
} else {
LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!")
}
}
}.await()
if (targetChunkState != ChunkState.FULL) {
// and finally, schedule chunks to be loaded into FULL state
// this way, dungeons won't get cut off when chunks being saved
// to disk because of dungeon bleeding into neighbour chunks who
// never get promoted further
// But this might trigger cascading world generation
// (dungeon generates another dungeon, and another, and so on),
// tough, so need to take care!
for (box in boundingBoxes) {
// specify timer as 0 so ticket gets removed on next world tick
parent.temporaryChunkTicket(box, 0, ChunkState.FULL)
}
} }
} finally { } finally {
tickets.forEach { it.cancel() } tickets.forEach { it.cancel() }
} }
} }
companion object{ companion object {
private val LOGGER = LogManager.getLogger()
private val offsets = listOf(Vector2i.POSITIVE_Y, Vector2i.NEGATIVE_Y, Vector2i.POSITIVE_X, Vector2i.NEGATIVE_X) private val offsets = listOf(Vector2i.POSITIVE_Y, Vector2i.NEGATIVE_Y, Vector2i.POSITIVE_X, Vector2i.NEGATIVE_X)
} }
} }

View File

@ -37,7 +37,8 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
for (y in 0 until image.height) { for (y in 0 until image.height) {
val offset = (x + y * image.width) * channels val offset = (x + y * image.width) * channels
tileData[x + y * image.width] = bytes[offset].toInt().and(0xFF) or // flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
bytes[offset + 1].toInt().and(0xFF).shl(8) or bytes[offset + 1].toInt().and(0xFF).shl(8) or
bytes[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255 bytes[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255
} }
@ -49,7 +50,8 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
for (y in 0 until image.height) { for (y in 0 until image.height) {
val offset = (x + y * image.width) * channels val offset = (x + y * image.width) * channels
tileData[x + y * image.width] = bytes[offset].toInt().and(0xFF) or // flip image as we go
tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or
bytes[offset + 1].toInt().and(0xFF).shl(8) or bytes[offset + 1].toInt().and(0xFF).shl(8) or
bytes[offset + 2].toInt().and(0xFF).shl(16) or bytes[offset + 2].toInt().and(0xFF).shl(16) or
bytes[offset + 3].toInt().and(0xFF).shl(24) bytes[offset + 3].toInt().and(0xFF).shl(24)
@ -93,6 +95,7 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> { override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (layer in layers) { for (layer in layers) {
// walk bottom-top first, this way we will place bottom objects/tiles before top ones
for (y in 0 until layer.data.height) { for (y in 0 until layer.data.height) {
for (x in 0 until layer.data.width) { for (x in 0 until layer.data.width) {
val get = callback(x, y, layer.palette[layer.data[x, y]]) val get = callback(x, y, layer.palette[layer.data[x, y]])

View File

@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.json.getAdapter import ru.dbotthepony.kstarbound.json.getAdapter
import java.util.LinkedList
// dungeons are stored as images, and each pixel // dungeons are stored as images, and each pixel
// represents a different tile. To make sense // represents a different tile. To make sense
@ -19,6 +20,8 @@ import ru.dbotthepony.kstarbound.json.getAdapter
class ImageTileSet(list: List<DungeonTile> = listOf()) { class ImageTileSet(list: List<DungeonTile> = listOf()) {
private val mapping = Int2ObjectOpenHashMap<DungeonTile>() private val mapping = Int2ObjectOpenHashMap<DungeonTile>()
private val warnings = LinkedList<String>()
init { init {
for ((i, it) in list.withIndex()) { for ((i, it) in list.withIndex()) {
val replaced = mapping.put(it.index, it) val replaced = mapping.put(it.index, it)
@ -26,11 +29,19 @@ class ImageTileSet(list: List<DungeonTile> = listOf()) {
// allow duplicates of same entry because vanilla files have them. // allow duplicates of same entry because vanilla files have them.
if (replaced != null && replaced != it) { if (replaced != null && replaced != it) {
val color = RGBAColor.abgr(it.index) val color = RGBAColor.abgr(it.index)
throw IllegalArgumentException("Two tiles are trying to take same place with index ${it.index} [${color.redInt}, ${color.greenInt}, ${color.blueInt}, ${color.alphaInt}] (list index $i):\ntile 1: $replaced\ntile 2: $it") warnings.add("Duplicate tile [${color.redInt}, ${color.greenInt}, ${color.blueInt}, ${color.alphaInt}], overwriting: $replaced -> $it")
} }
} }
} }
fun spewWarnings(prefix: String) {
warnings.forEach {
LOGGER.warn("$prefix: $it")
}
warnings.clear()
}
operator fun get(index: Int): DungeonTile? { operator fun get(index: Int): DungeonTile? {
return mapping.get(index) return mapping.get(index)
} }

View File

@ -225,8 +225,9 @@ class TiledMap(data: JsonData) : TileMap() {
} }
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> { override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (x in this.x until this.x + width) { // walk bottom-top first, this way we will place bottom objects/tiles before top ones
for (y in this.y until this.y + height) { for (y in this.y until this.y + height) {
for (x in this.x until this.x + width) {
val result = callback(x, y, get0(x, y)) val result = callback(x, y, get0(x, y))
if (result.isPresent) return result if (result.isPresent) return result
} }
@ -418,8 +419,8 @@ class TiledMap(data: JsonData) : TileMap() {
ObjType.RECTANGLE -> { ObjType.RECTANGLE -> {
// Used for creating custom brushes and rules // Used for creating custom brushes and rules
for (x in this.pos.x until this.pos.x + this.size.x) { for (y in this.pos.y until this.pos.y + this.size.y) {
for (y in this.pos.y until this.pos.y + this.size.y) { for (x in this.pos.x until this.pos.x + this.size.x) {
val result = callback(x, y, tile) val result = callback(x, y, tile)
if (result.isPresent) return result if (result.isPresent) return result
} }

View File

@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
@ -15,13 +16,15 @@ import ru.dbotthepony.kstarbound.set
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
class TiledTileSet private constructor( class TiledTileSet private constructor(
val front: ImmutableMap<Int, Pair<DungeonTile, JsonObject>>, // plz dont modify :)
val back: ImmutableMap<Int, Pair<DungeonTile, JsonObject>>, // used for speed
val front: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>,
val back: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>,
) { ) {
@JsonFactory @JsonFactory
data class JsonData( data class JsonData(
val properties: JsonObject = JsonObject(), val properties: JsonObject = JsonObject(),
// val tilecount: Int, // we don't care val tilecount: Int,
val tileproperties: JsonObject = JsonObject(), // empty tileset? val tileproperties: JsonObject = JsonObject(), // empty tileset?
) )
@ -59,8 +62,8 @@ class TiledTileSet private constructor(
try { try {
val data = Starbound.gson.fromJson(locate, JsonData::class.java) val data = Starbound.gson.fromJson(locate, JsonData::class.java)
val front = ImmutableMap.Builder<Int, Pair<DungeonTile, JsonObject>>() val front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(data.tilecount + 40)
val back = ImmutableMap.Builder<Int, Pair<DungeonTile, JsonObject>>() val back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(data.tilecount + 40)
for ((key, value) in data.tileproperties.entrySet()) { for ((key, value) in data.tileproperties.entrySet()) {
if (value !is JsonObject) if (value !is JsonObject)
@ -88,7 +91,7 @@ class TiledTileSet private constructor(
back[index] = makeTile(mergeBack) to mergeBack back[index] = makeTile(mergeBack) to mergeBack
} }
return Either.left(TiledTileSet(front.build(), back.build())) return Either.left(TiledTileSet(front, back))
} catch (err: Throwable) { } catch (err: Throwable) {
return Either.right(err) return Either.right(err)
} }

View File

@ -14,11 +14,11 @@ class TiledTileSets(entries: List<Entry>) {
val source: String, val source: String,
) )
private var front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>() private var front: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>
private var back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>() private var back: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>
init { init {
for ((firstgid, source) in entries) { val mapped = entries.map { (firstgid, source) ->
// Tiled stores tileset paths relative to the map file, which can go below // Tiled stores tileset paths relative to the map file, which can go below
// the assets root if it's referencing a tileset in another asset package. // the assets root if it's referencing a tileset in another asset package.
// The solution chosen here is to ignore everything in the path up until a // The solution chosen here is to ignore everything in the path up until a
@ -36,11 +36,16 @@ class TiledTileSets(entries: List<Entry>) {
actualSource = AssetPathStack.remap(source) actualSource = AssetPathStack.remap(source)
} }
val set = TiledTileSet.load(actualSource) firstgid to TiledTileSet.load(actualSource)
}
front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(mapped.sumOf { it.second.size })
back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(mapped.sumOf { it.second.size })
for ((firstgid, set) in mapped) {
for (i in 0 until set.size) { for (i in 0 until set.size) {
front[firstgid + i] = set.front[i] ?: throw NullPointerException("aeiou") front.put(firstgid + i, set.front[i] ?: throw NullPointerException("aeiou"))
back[firstgid + i] = set.back[i] ?: throw NullPointerException("aeiou") back.put(firstgid + i, set.back[i] ?: throw NullPointerException("aeiou"))
} }
} }
} }

View File

@ -1,5 +1,26 @@
package ru.dbotthepony.kstarbound.defs.`object` package ru.dbotthepony.kstarbound.defs.`object`
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.world.World
data class Anchor(val isForeground: Boolean, val pos: Vector2i, val isTilled: Boolean, val isSoil: Boolean, val anchorMaterial: String?) data class Anchor(val isBackground: Boolean, val position: Vector2i, val isTilled: Boolean, val isSoil: Boolean, val anchorMaterial: Registry.Ref<TileDefinition>?) {
fun isValidPlacement(world: World<*, *>, position: Vector2i): Boolean {
val cell = world.chunkMap.getCell(position + this.position)
if (!cell.isConnectible(false, isBackground))
return false
if (isTilled && !cell.tile(isBackground).modifier.value.tilled)
return false
if (isSoil && !cell.tile(isBackground).material.value.soil)
return false
if (anchorMaterial?.entry != null && cell.tile(isBackground).material != anchorMaterial.entry)
return false
return true
}
}

View File

@ -29,9 +29,12 @@ import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
@JsonAdapter(ObjectDefinition.Adapter::class) @JsonAdapter(ObjectDefinition.Adapter::class)
data class ObjectDefinition( data class ObjectDefinition(
@ -51,7 +54,7 @@ data class ObjectDefinition(
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null, val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(), val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
val animation: AssetReference<AnimationDefinition>? = null, val animation: AssetReference<AnimationDefinition>? = null,
//val animation: AssetPath? = null, val animationCustom: JsonObject = JsonObject(),
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(), val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
val smashParticles: JsonArray? = null, val smashParticles: JsonArray? = null,
val smashable: Boolean = false, val smashable: Boolean = false,
@ -81,6 +84,28 @@ data class ObjectDefinition(
val flickerPeriod: PeriodicFunction? = null, val flickerPeriod: PeriodicFunction? = null,
val orientations: ImmutableList<ObjectOrientation>, val orientations: ImmutableList<ObjectOrientation>,
) { ) {
fun findValidOrientation(world: World<*, *>, position: Vector2i, directionAffinity: Direction? = null): Int {
// If we are given a direction affinity, try and find an orientation with a
// matching affinity *first*
if (directionAffinity != null) {
for ((i, orientation) in orientations.withIndex()) {
if (orientation.directionAffinity == null || orientation.directionAffinity != directionAffinity)
continue
if (orientation.placementValid(world, position) && orientation.anchorsValid(world, position))
return i
}
}
// Then, fallback and try and find any valid affinity
for ((i, orientation) in orientations.withIndex()) {
if (orientation.placementValid(world, position) && orientation.anchorsValid(world, position))
return i
}
return -1
}
class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() { class Adapter(gson: Gson) : TypeAdapter<ObjectDefinition>() {
@JsonFactory(logMisses = false) @JsonFactory(logMisses = false)
data class PlainData( data class PlainData(
@ -100,6 +125,7 @@ data class ObjectDefinition(
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null, val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(), val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
val animation: AssetReference<AnimationDefinition>? = null, val animation: AssetReference<AnimationDefinition>? = null,
val animationCustom: JsonObject = JsonObject(),
//val animation: AssetPath? = null, //val animation: AssetPath? = null,
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(), val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
val smashParticles: JsonArray? = null, val smashParticles: JsonArray? = null,
@ -203,6 +229,7 @@ data class ObjectDefinition(
smashDropPool = basic.smashDropPool, smashDropPool = basic.smashDropPool,
smashDropOptions = basic.smashDropOptions, smashDropOptions = basic.smashDropOptions,
animation = basic.animation, animation = basic.animation,
animationCustom = basic.animationCustom,
smashSounds = basic.smashSounds, smashSounds = basic.smashSounds,
smashParticles = basic.smashParticles, smashParticles = basic.smashParticles,
smashable = basic.smashable, smashable = basic.smashable,

View File

@ -31,7 +31,11 @@ import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.Side import ru.dbotthepony.kstarbound.world.Side
import ru.dbotthepony.kstarbound.world.World
import kotlin.math.PI import kotlin.math.PI
@JsonAdapter(ObjectOrientation.Adapter::class) @JsonAdapter(ObjectOrientation.Adapter::class)
@ -49,7 +53,7 @@ data class ObjectOrientation(
val metaBoundBox: AABB?, val metaBoundBox: AABB?,
val anchors: ImmutableSet<Anchor>, val anchors: ImmutableSet<Anchor>,
val anchorAny: Boolean, val anchorAny: Boolean,
val directionAffinity: Side?, val directionAffinity: Direction?,
val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>, val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>,
val interactiveSpaces: ImmutableSet<Vector2i>, val interactiveSpaces: ImmutableSet<Vector2i>,
val lightPosition: Vector2i, val lightPosition: Vector2i,
@ -58,6 +62,38 @@ data class ObjectOrientation(
val touchDamage: JsonReference.Object?, val touchDamage: JsonReference.Object?,
val particleEmitters: ArrayList<ParticleEmissionEntry>, val particleEmitters: ArrayList<ParticleEmissionEntry>,
) { ) {
fun placementValid(world: World<*, *>, position: Vector2i): Boolean {
if (occupySpaces.isEmpty())
return true
return occupySpaces.all {
val cell = world.chunkMap.getCell(it + position)
//if (!cell.foreground.material.isEmptyTile) println("not empty tile: ${it + position}, space $it, pos $position")
//if (cell.dungeonId in world.protectedDungeonIDs) println("position is protected: ${it + position}")
cell.foreground.material.isEmptyTile && cell.dungeonId !in world.protectedDungeonIDs
}
}
fun anchorsValid(world: World<*, *>, position: Vector2i): Boolean {
if (anchors.isEmpty())
return true
var anyValid = false
for (anchor in anchors) {
val isValid = anchor.isValidPlacement(world, position)
if (isValid)
anyValid = true
else if (!anchorAny) {
// println("anchor $anchor reported false for $position ${world.chunkMap.getCell(position + anchor.position)}")
return false
}
}
return anyValid
}
companion object { companion object {
fun preprocess(json: JsonArray): JsonArray { fun preprocess(json: JsonArray): JsonArray {
val actual = ArrayList<JsonObject>() val actual = ArrayList<JsonObject>()
@ -197,16 +233,16 @@ data class ObjectOrientation(
val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) } val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) }
val requireTilledAnchors = obj.get("requireTilledAnchors", false) val requireTilledAnchors = obj.get("requireTilledAnchors", false)
val requireSoilAnchors = obj.get("requireSoilAnchors", false) val requireSoilAnchors = obj.get("requireSoilAnchors", false)
val anchorMaterial = obj["anchorMaterial"]?.asString val anchorMaterial = obj["anchorMaterial"]?.asString?.let { Registries.tiles.ref(it) }
val anchors = ImmutableSet.Builder<Anchor>() val anchors = ImmutableSet.Builder<Anchor>()
for (v in obj.get("anchors", JsonArray())) { for (v in obj.get("anchors", JsonArray())) {
when (v.asString.lowercase()) { when (v.asString.lowercase()) {
"left" -> occupySpaces.stream().filter { it.x == boundingBox.mins.x }.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "left" -> occupySpaces.stream().filter { it.x == boundingBox.mins.x }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"right" -> occupySpaces.stream().filter { it.x == boundingBox.maxs.x }.forEach { anchors.add(Anchor(true, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "right" -> occupySpaces.stream().filter { it.x == boundingBox.maxs.x }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_X, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"top" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(true, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "top" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(false, it + Vector2i.POSITIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"bottom" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "bottom" -> occupySpaces.stream().filter { it.y == boundingBox.mins.y }.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"background" -> occupySpaces.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) } "background" -> occupySpaces.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
else -> throw JsonSyntaxException("Unknown anchor type $v") else -> throw JsonSyntaxException("Unknown anchor type $v")
} }
} }
@ -218,7 +254,7 @@ data class ObjectOrientation(
anchors.add(Anchor(true, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial)) anchors.add(Anchor(true, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial))
val anchorAny = obj["anchorAny"]?.asBoolean ?: false val anchorAny = obj["anchorAny"]?.asBoolean ?: false
val directionAffinity = obj["directionAffinity"]?.asString?.uppercase()?.let { Side.valueOf(it) } val directionAffinity = obj["direction"]?.asString?.uppercase()?.let { Direction.valueOf(it) }
val materialSpaces: ImmutableList<Pair<Vector2i, String>> val materialSpaces: ImmutableList<Pair<Vector2i, String>>
if ("materialSpaces" in obj) { if ("materialSpaces" in obj) {
@ -227,7 +263,7 @@ data class ObjectOrientation(
val collisionSpaces = obj["collisionSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces val collisionSpaces = obj["collisionSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces
val builder = ImmutableList.Builder<Pair<Vector2i, String>>() val builder = ImmutableList.Builder<Pair<Vector2i, String>>()
when (val collisionType = obj.get("collisionType", "none").lowercase()) { when (val collisionType = obj.get("collision", "none").lowercase()) {
"solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.key) } "solid" -> collisionSpaces.forEach { builder.add(it to BuiltinMetaMaterials.OBJECT_SOLID.key) }
"platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) } "platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) }
"none" -> {} "none" -> {}

View File

@ -101,13 +101,14 @@ const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524
const val FIRST_RESERVED_DUNGEON_ID = 65520 const val FIRST_RESERVED_DUNGEON_ID = 65520
object BuiltinMetaMaterials { object BuiltinMetaMaterials {
private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition( private fun make(id: Int, name: String, collisionType: CollisionType, isConnectable: Boolean = true) = Registries.tiles.add(name, id, TileDefinition(
materialId = id, materialId = id,
materialName = "metamaterial:$name", materialName = "metamaterial:$name",
descriptionData = ThingDescription.EMPTY, descriptionData = ThingDescription.EMPTY,
category = "meta", category = "meta",
renderTemplate = AssetReference.empty(), renderTemplate = AssetReference.empty(),
renderParameters = RenderParameters.META, renderParameters = RenderParameters.META,
isConnectable = isConnectable,
isMeta = true, isMeta = true,
supportsMods = false, supportsMods = false,
collisionKind = collisionType, collisionKind = collisionType,
@ -132,12 +133,12 @@ object BuiltinMetaMaterials {
/** /**
* air * air
*/ */
val EMPTY = make(65535, "empty", CollisionType.NONE) val EMPTY = make(65535, "empty", CollisionType.NONE, isConnectable = false)
/** /**
* not set / out of bounds * not set / out of bounds
*/ */
val NULL = make(65534, "null", CollisionType.NULL) val NULL = make(65534, "null", CollisionType.NULL, isConnectable = false)
val STRUCTURE = make(65533, "structure", CollisionType.BLOCK) val STRUCTURE = make(65533, "structure", CollisionType.BLOCK)
val BIOME = make(65527, "biome", CollisionType.BLOCK) val BIOME = make(65527, "biome", CollisionType.BLOCK)
@ -146,7 +147,7 @@ object BuiltinMetaMaterials {
val BIOME3 = make(65530, "biome3", CollisionType.BLOCK) val BIOME3 = make(65530, "biome3", CollisionType.BLOCK)
val BIOME4 = make(65531, "biome4", CollisionType.BLOCK) val BIOME4 = make(65531, "biome4", CollisionType.BLOCK)
val BIOME5 = make(65532, "biome5", CollisionType.BLOCK) val BIOME5 = make(65532, "biome5", CollisionType.BLOCK)
val BOUNDARY = make(65526, "boundary", CollisionType.SLIPPERY) val BOUNDARY = make(65526, "boundary", CollisionType.SLIPPERY, isConnectable = false)
val OBJECT_SOLID = make(65500, "objectsolid", CollisionType.BLOCK) val OBJECT_SOLID = make(65500, "objectsolid", CollisionType.BLOCK)
val OBJECT_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM) val OBJECT_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM)

View File

@ -31,6 +31,8 @@ data class TileDefinition(
val health: Double? = null, val health: Double? = null,
val requiredHarvestLevel: Int? = null, val requiredHarvestLevel: Int? = null,
val isConnectable: Boolean = true,
@JsonFlat @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,

View File

@ -18,6 +18,7 @@ data class TileModifierDefinition(
val requiredHarvestLevel: Int? = null, val requiredHarvestLevel: Int? = null,
val breaksWithTile: Boolean = true, val breaksWithTile: Boolean = true,
val grass: Boolean = false, val grass: Boolean = false,
val tilled: Boolean = false,
val miningParticle: String? = null, val miningParticle: String? = null,
val footstepSound: String? = null, val footstepSound: String? = null,

View File

@ -17,10 +17,12 @@ import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.stream import ru.dbotthepony.kommons.gson.stream
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
@ -114,9 +116,7 @@ data class BiomePlaceables(
// Truly our hero here. // Truly our hero here.
val obj = when (val type = `in`.nextString()) { val obj = when (val type = `in`.nextString()) {
"treasureBoxSet" -> TreasureBox(`in`.nextString()) "treasureBoxSet" -> TreasureBox(`in`.nextString())
"microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { it.asString }.collect( "microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet()))
ImmutableSet.toImmutableSet()
))
"grass" -> Grass(grassVariant.read(`in`)) "grass" -> Grass(grassVariant.read(`in`))
"bush" -> Bush(bushVariant.read(`in`)) "bush" -> Bush(bushVariant.read(`in`))
"treePair" -> Tree(trees.read(`in`)) "treePair" -> Tree(trees.read(`in`))
@ -135,13 +135,13 @@ data class BiomePlaceables(
} }
} }
data class MicroDungeon(val microdungeons: ImmutableSet<String> = ImmutableSet.of()) : Item() { data class MicroDungeon(val microdungeons: ImmutableSet<Registry.Ref<DungeonDefinition>> = ImmutableSet.of()) : Item() {
override val type: BiomePlacementItemType override val type: BiomePlacementItemType
get() = BiomePlacementItemType.MICRO_DUNGEON get() = BiomePlacementItemType.MICRO_DUNGEON
override fun toJson(): JsonElement { override fun toJson(): JsonElement {
return JsonArray().also { j -> return JsonArray().also { j ->
microdungeons.forEach { j.add(JsonPrimitive(it)) } microdungeons.forEach { j.add(JsonPrimitive(it.key.left())) }
} }
} }
} }

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
@ -62,7 +63,7 @@ data class BiomePlaceablesDefinition(
} }
@JsonFactory @JsonFactory
data class MicroDungeon(val microdungeons: ImmutableSet<String> = ImmutableSet.of()) : DistributionItemData() { data class MicroDungeon(val microdungeons: ImmutableSet<Registry.Ref<DungeonDefinition>> = ImmutableSet.of()) : DistributionItemData() {
override val type: BiomePlacementItemType override val type: BiomePlacementItemType
get() = BiomePlacementItemType.MICRO_DUNGEON get() = BiomePlacementItemType.MICRO_DUNGEON

View File

@ -5,8 +5,10 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
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.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
@ -25,7 +27,7 @@ data class DungeonWorldsConfig(
val dungeonBaseHeight: Int, val dungeonBaseHeight: Int,
val dungeonSurfaceHeight: Int = dungeonBaseHeight, val dungeonSurfaceHeight: Int = dungeonBaseHeight,
val dungeonUndergroundLevel: Int = 0, val dungeonUndergroundLevel: Int = 0,
val primaryDungeon: String, val primaryDungeon: Registry.Ref<DungeonDefinition>,
val biome: String? = null, val biome: String? = null,
val ambientLightLevel: RGBAColor, val ambientLightLevel: RGBAColor,

View File

@ -7,7 +7,9 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.io.readColor import ru.dbotthepony.kstarbound.io.readColor
import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readNullableString import ru.dbotthepony.kstarbound.io.readNullableString
@ -27,7 +29,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
private set private set
var dungeonUndergroundLevel: Int by Delegates.notNull() var dungeonUndergroundLevel: Int by Delegates.notNull()
private set private set
var primaryDungeon: String by Delegates.notNull() var primaryDungeon: Registry.Ref<DungeonDefinition> by Delegates.notNull()
private set private set
var ambientLightLevel: RGBAColor by Delegates.notNull() var ambientLightLevel: RGBAColor by Delegates.notNull()
private set private set
@ -71,9 +73,10 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
@JsonFactory @JsonFactory
data class JsonData( data class JsonData(
val dungeonBaseHeight: Int,
val dungeonSurfaceHeight: Int, val dungeonSurfaceHeight: Int,
val dungeonUndergroundLevel: Int, val dungeonUndergroundLevel: Int,
val primaryDungeon: String, val primaryDungeon: Registry.Ref<DungeonDefinition>,
val biome: String? = null, val biome: String? = null,
val ambientLightLevel: RGBAColor, val ambientLightLevel: RGBAColor,
val dayMusicTrack: String? = null, val dayMusicTrack: String? = null,
@ -87,6 +90,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
val read = Starbound.gson.fromJson(data, JsonData::class.java) val read = Starbound.gson.fromJson(data, JsonData::class.java)
dungeonBaseHeight = read.dungeonBaseHeight
dungeonSurfaceHeight = read.dungeonSurfaceHeight dungeonSurfaceHeight = read.dungeonSurfaceHeight
dungeonUndergroundLevel = read.dungeonUndergroundLevel dungeonUndergroundLevel = read.dungeonUndergroundLevel
primaryDungeon = read.primaryDungeon primaryDungeon = read.primaryDungeon
@ -102,7 +106,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
super.toJson(data, isLegacy) super.toJson(data, isLegacy)
val serialize = Starbound.gson.toJsonTree(JsonData( val serialize = Starbound.gson.toJsonTree(JsonData(
dungeonSurfaceHeight, dungeonUndergroundLevel, primaryDungeon, biome, ambientLightLevel, dayMusicTrack, nightMusicTrack, dayAmbientNoises, nightAmbientNoises dungeonBaseHeight, dungeonSurfaceHeight, dungeonUndergroundLevel, primaryDungeon, biome, ambientLightLevel, dayMusicTrack, nightMusicTrack, dayAmbientNoises, nightAmbientNoises
)) as JsonObject )) as JsonObject
for ((k, v) in serialize.entrySet()) { for ((k, v) in serialize.entrySet()) {
@ -116,7 +120,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
dungeonBaseHeight = stream.readInt() dungeonBaseHeight = stream.readInt()
dungeonSurfaceHeight = stream.readInt() dungeonSurfaceHeight = stream.readInt()
dungeonUndergroundLevel = stream.readInt() dungeonUndergroundLevel = stream.readInt()
primaryDungeon = stream.readInternedString() primaryDungeon = Registries.dungeons.ref(stream.readInternedString())
biome = stream.readNullableString() biome = stream.readNullableString()
ambientLightLevel = stream.readColor() ambientLightLevel = stream.readColor()
dayMusicTrack = stream.readNullableString() dayMusicTrack = stream.readNullableString()
@ -131,7 +135,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
stream.writeInt(dungeonBaseHeight) stream.writeInt(dungeonBaseHeight)
stream.writeInt(dungeonSurfaceHeight) stream.writeInt(dungeonSurfaceHeight)
stream.writeInt(dungeonUndergroundLevel) stream.writeInt(dungeonUndergroundLevel)
stream.writeBinaryString(primaryDungeon) stream.writeBinaryString(primaryDungeon.key.left())
stream.writeNullableString(biome) stream.writeNullableString(biome)
stream.writeColor(ambientLightLevel) stream.writeColor(ambientLightLevel)
stream.writeNullableString(dayMusicTrack) stream.writeNullableString(dayMusicTrack)
@ -145,6 +149,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!") val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!")
val parameters = FloatingDungeonWorldParameters() val parameters = FloatingDungeonWorldParameters()
parameters.typeName = typeName
parameters.worldSize = config.worldSize parameters.worldSize = config.worldSize
parameters.threatLevel = config.threatLevel parameters.threatLevel = config.threatLevel
parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it }) parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it })

View File

@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid
@ -33,6 +34,7 @@ import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeStruct2d import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
@ -131,7 +133,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val layerMinHeight: Int, val layerMinHeight: Int,
val layerBaseHeight: Int, val layerBaseHeight: Int,
val dungeons: ImmutableSet<String>, val dungeons: ImmutableSet<Registry.Ref<DungeonDefinition>>,
val dungeonXVariance: Int, val dungeonXVariance: Int,
val primaryRegion: Region, val primaryRegion: Region,
@ -146,7 +148,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
constructor(stream: DataInputStream) : this( constructor(stream: DataInputStream) : this(
stream.readInt(), stream.readInt(),
stream.readInt(), stream.readInt(),
ImmutableSet.copyOf(stream.readCollection { readInternedString() }), ImmutableSet.copyOf(stream.readCollection { Registries.dungeons.ref(readInternedString()) }),
stream.readInt(), stream.readInt(),
Region(stream), Region(stream),
@ -163,7 +165,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
stream.writeInt(layerMinHeight) stream.writeInt(layerMinHeight)
stream.writeInt(layerBaseHeight) stream.writeInt(layerBaseHeight)
stream.writeCollection(dungeons) { writeBinaryString(it) } stream.writeCollection(dungeons) { writeBinaryString(it.key.left()) }
stream.writeInt(dungeonXVariance) stream.writeInt(dungeonXVariance)
primaryRegion.write(stream) primaryRegion.write(stream)
@ -287,6 +289,18 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
var coreLayer: Layer by Delegates.notNull() var coreLayer: Layer by Delegates.notNull()
private set private set
val layers: List<Layer> get() {
val layers = ArrayList<Layer>()
layers.add(spaceLayer)
layers.add(atmosphereLayer)
layers.add(surfaceLayer)
layers.addAll(undergroundLayers)
layers.add(coreLayer)
return layers
}
override val type: VisitableWorldParametersType override val type: VisitableWorldParametersType
get() = VisitableWorldParametersType.TERRESTRIAL get() = VisitableWorldParametersType.TERRESTRIAL
@ -397,7 +411,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
private val biomePairs by lazy { Starbound.gson.pairAdapter<Double, JsonArray>() } private val biomePairs by lazy { Starbound.gson.pairAdapter<Double, JsonArray>() }
private val vectors2d by lazy { Starbound.gson.getAdapter(Vector2d::class.java) } private val vectors2d by lazy { Starbound.gson.getAdapter(Vector2d::class.java) }
private val vectors2i by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } private val vectors2i by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
private val dungeonPools by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(WeightedList::class.java, String::class.java)) as TypeAdapter<WeightedList<String>> } private val dungeonPools by lazy { Starbound.gson.getAdapter<WeightedList<Registry.Ref<DungeonDefinition>>>() }
fun generate(typeName: String, sizeName: String, seed: Long): TerrestrialWorldParameters { fun generate(typeName: String, sizeName: String, seed: Long): TerrestrialWorldParameters {
return generate(typeName, sizeName, random(seed)) return generate(typeName, sizeName, random(seed))

View File

@ -118,6 +118,9 @@ class WorldLayout {
} }
fun findContainingCell(x: Int): Pair<Int, Int> { fun findContainingCell(x: Int): Pair<Int, Int> {
if (boundaries.isEmpty) // entire world strip is within single cell
return 0 to worldGeometry.x.cell(x)
val wx = worldGeometry.x.cell(x) val wx = worldGeometry.x.cell(x)
if (wx < boundaries.first()) { if (wx < boundaries.first()) {

View File

@ -1,8 +1,8 @@
package ru.dbotthepony.kstarbound.defs.world package ru.dbotthepony.kstarbound.defs.world
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.JsonObject import com.google.gson.JsonObject
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
@ -10,6 +10,7 @@ import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
@ -19,11 +20,12 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.math.quintic2 import ru.dbotthepony.kstarbound.math.quintic2
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64 import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import java.time.Duration import java.util.concurrent.CopyOnWriteArrayList
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
class WorldTemplate(val geometry: WorldGeometry) { class WorldTemplate(val geometry: WorldGeometry) {
@ -43,16 +45,20 @@ class WorldTemplate(val geometry: WorldGeometry) {
val threatLevel: Double val threatLevel: Double
get() = worldParameters?.threatLevel ?: 0.0 get() = worldParameters?.threatLevel ?: 0.0
private val customTerrainRegions = ArrayList<CustomTerrainRegion>() // CoW because these are updated by dungeons across threads (we can't synchronize these updates
// because tile generation always happen on worker threads)
private val customTerrainRegions = CopyOnWriteArrayList<CustomTerrainRegion>()
constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) { constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) {
this.seed = seed this.seed = seed
this.worldParameters = worldParameters
this.skyParameters = skyParameters this.skyParameters = skyParameters
this.worldLayout = worldParameters.createLayout(seed) this.worldLayout = worldParameters.createLayout(seed)
} }
constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, random: RandomGenerator) : this(WorldGeometry(worldParameters.worldSize, true, false)) { constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, random: RandomGenerator) : this(WorldGeometry(worldParameters.worldSize, true, false)) {
this.seed = random.nextLong() this.seed = random.nextLong()
this.worldParameters = worldParameters
this.skyParameters = skyParameters this.skyParameters = skyParameters
this.worldLayout = worldParameters.createLayout(random) this.worldLayout = worldParameters.createLayout(random)
} }
@ -227,6 +233,50 @@ class WorldTemplate(val geometry: WorldGeometry) {
return result return result
} }
data class Dungeon(
val dungeon: Registry.Entry<DungeonDefinition>,
val baseHeight: Int,
val baseX: Int,
val xVariance: Int,
val forcePlacement: Boolean,
val blendWithTerrain: Boolean,
)
fun gatherDungeons(): List<Dungeon> {
val dungeons = ArrayList<Dungeon>()
when (val worldParameters = worldParameters) {
is FloatingDungeonWorldParameters -> {
if (worldParameters.primaryDungeon.isPresent) {
dungeons.add(Dungeon(worldParameters.primaryDungeon.entry!!, worldParameters.dungeonBaseHeight, 0, 0, true, false))
} else {
LOGGER.error("Floating dungeon world's primary dungeon ${worldParameters.primaryDungeon.key.left()} is missing... What happens now?")
}
}
is TerrestrialWorldParameters -> {
for (layer in worldParameters.layers) {
if (layer.dungeons.isNotEmpty()) {
val dungeonSpacing = geometry.size.x / layer.dungeons.size
var dungeonOffset = staticRandomInt(0, geometry.size.x, seed, layer.layerBaseHeight, "dungeon")
for (dungeon in layer.dungeons) {
if (dungeon.isPresent) {
dungeons.add(Dungeon(dungeon.entry!!, layer.layerBaseHeight, dungeonOffset, layer.dungeonXVariance, false, true))
dungeonOffset += dungeonSpacing
dungeonOffset = geometry.x.cell(dungeonOffset)
} else {
LOGGER.error("Primary dungeon ${dungeon.key.left()} at layer Y ${layer.layerBaseHeight} is missing")
}
}
}
}
}
}
return dungeons
}
class CellInfo(val x: Int, val y: Int) { class CellInfo(val x: Int, val y: Int) {
var foreground: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref var foreground: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
var foregroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref var foregroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref
@ -455,6 +505,8 @@ class WorldTemplate(val geometry: WorldGeometry) {
} }
companion object { companion object {
private val LOGGER = LogManager.getLogger()
suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate { suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate {
val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!") val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!")
val visitable = params.visitableParameters ?: throw IllegalArgumentException("$coordinate of $universe is not visitable") val visitable = params.visitableParameters ?: throw IllegalArgumentException("$coordinate of $universe is not visitable")

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -28,7 +29,7 @@ class EntityDestroyPacket(val entityID: Int, val finalNetState: ByteArrayList, v
connection.disconnect("Removing entity with ID $entityID outside of allowed range ${connection.entityIDRange}") connection.disconnect("Removing entity with ID $entityID outside of allowed range ${connection.entityIDRange}")
} else { } else {
connection.enqueue { connection.enqueue {
entities[entityID]?.remove() entities[entityID]?.remove(if (isDeath) AbstractEntity.RemovalReason.REMOTE_DYING else AbstractEntity.RemovalReason.REMOTE_REMOVAL)
} }
} }
} }

View File

@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.collect.RandomListIterator
import ru.dbotthepony.kstarbound.collect.RandomSubList import ru.dbotthepony.kstarbound.collect.RandomSubList
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList
// original engine does not have "networked list", so it is always networked // original engine does not have "networked list", so it is always networked
// the dumb way on legacy protocol // the dumb way on legacy protocol
@ -42,6 +43,11 @@ class NetworkedList<E>(
private var isInterpolating = false private var isInterpolating = false
private var currentTime = 0.0 private var currentTime = 0.0
private var isRemote = false private var isRemote = false
private val listeners = CopyOnWriteArrayList<Runnable>()
fun addListener(listener: Runnable) {
listeners.add(listener)
}
private fun purgeBacklog() { private fun purgeBacklog() {
while (backlog.size >= maxBacklogSize) { while (backlog.size >= maxBacklogSize) {
@ -89,6 +95,7 @@ class NetworkedList<E>(
} }
purgeBacklog() purgeBacklog()
listeners.forEach { it.run() }
} }
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
@ -139,6 +146,7 @@ class NetworkedList<E>(
} }
purgeBacklog() purgeBacklog()
listeners.forEach { it.run() }
} else { } else {
readInitial(data, false) readInitial(data, false)
} }
@ -235,6 +243,7 @@ class NetworkedList<E>(
elements.add(index, element) elements.add(index, element)
backlog.add(currentVersion() to Entry(index, element)) backlog.add(currentVersion() to Entry(index, element))
purgeBacklog() purgeBacklog()
listeners.forEach { it.run() }
} }
override fun addAll(index: Int, elements: Collection<E>): Boolean { override fun addAll(index: Int, elements: Collection<E>): Boolean {
@ -252,6 +261,7 @@ class NetworkedList<E>(
backlog.clear() backlog.clear()
backlog.add(currentVersion() to clearEntry) backlog.add(currentVersion() to clearEntry)
elements.clear() elements.clear()
listeners.forEach { it.run() }
} }
override fun listIterator(): MutableListIterator<E> { override fun listIterator(): MutableListIterator<E> {
@ -285,6 +295,7 @@ class NetworkedList<E>(
val element = elements.removeAt(index) val element = elements.removeAt(index)
backlog.add(currentVersion() to Entry(index)) backlog.add(currentVersion() to Entry(index))
purgeBacklog() purgeBacklog()
listeners.forEach { it.run() }
return element return element
} }
@ -308,6 +319,7 @@ class NetworkedList<E>(
val old = elements.set(index, element) val old = elements.set(index, element)
backlog.add(currentVersion() to Entry(index, element)) backlog.add(currentVersion() to Entry(index, element))
purgeBacklog() purgeBacklog()
listeners.forEach { it.run() }
return old return old
} }

View File

@ -52,8 +52,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>() private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
val universe = ServerUniverse() val universe = ServerUniverse()
val chat = ChatHandler(this) val chat = ChatHandler(this)
val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob()) val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
val eventLoopScope = CoroutineScope(asCoroutineDispatcher() + SupervisorJob())
private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>() private val systemWorlds = HashMap<Vector3i, CompletableFuture<ServerSystemWorld>>()
@ -64,7 +63,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> { fun loadSystemWorld(location: Vector3i): CompletableFuture<ServerSystemWorld> {
return supplyAsync { return supplyAsync {
systemWorlds.computeIfAbsent(location) { systemWorlds.computeIfAbsent(location) {
scope.async { loadSystemWorld0(location) }.asCompletableFuture() globalScope.async { loadSystemWorld0(location) }.asCompletableFuture()
} }
}.thenCompose { it } }.thenCompose { it }
} }
@ -89,6 +88,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld { private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld {
val config = Globals.instanceWorlds[location.name] ?: throw NoSuchElementException("No such instance world ${location.name}") val config = Globals.instanceWorlds[location.name] ?: throw NoSuchElementException("No such instance world ${location.name}")
LOGGER.info("Creating instance world $location")
val random = random(config.seed ?: System.nanoTime()) val random = random(config.seed ?: System.nanoTime())
val visitable = when (config.type.lowercase()) { val visitable = when (config.type.lowercase()) {
@ -111,9 +111,17 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
val template = WorldTemplate(visitable, config.skyParameters, random) val template = WorldTemplate(visitable, config.skyParameters, random)
val world = ServerWorld.create(this, template, WorldStorage.NULL, location) val world = ServerWorld.create(this, template, WorldStorage.NULL, location)
world.setProperty("ephemeral", JsonPrimitive(!config.persistent)) try {
world.setProperty("ephemeral", JsonPrimitive(!config.persistent))
world.eventLoop.start()
world.prepare().await()
} catch (err: Throwable) {
LOGGER.fatal("Exception while creating instance world at $location!", err)
world.eventLoop.shutdown()
throw err
}
world.eventLoop.start()
return world return world
} }
@ -138,7 +146,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
if (world != null) { if (world != null) {
world world
} else { } else {
val future = scope.async { loadWorld0(location) }.asCompletableFuture() val future = globalScope.async { loadWorld0(location) }.asCompletableFuture()
worlds[location] = future worlds[location] = future
future future
} }
@ -239,7 +247,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
return@removeIf false return@removeIf false
} }
eventLoopScope.launch { scope.launch {
try { try {
it.get().tick() it.get().tick()
} catch (err: Throwable) { } catch (err: Throwable) {
@ -289,6 +297,11 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
worlds.values.forEach { worlds.values.forEach {
if (it.isDone && !it.isCompletedExceptionally) { if (it.isDone && !it.isCompletedExceptionally) {
it.get().eventLoop.awaitTermination(10L, TimeUnit.SECONDS) it.get().eventLoop.awaitTermination(10L, TimeUnit.SECONDS)
if (!it.get().eventLoop.isTerminated) {
LOGGER.warn("World ${it.get()} did not shutdown in 10 seconds, forcing termination. This might leave world in inconsistent state!")
it.get().eventLoop.shutdownNow()
}
} }
it.cancel(true) it.cancel(true)

View File

@ -14,9 +14,7 @@ import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
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.Registries
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonRule
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.FIRST_RESERVED_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.MICRO_DUNGEON_ID
@ -33,16 +31,17 @@ 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.defs.world.Biome import ru.dbotthepony.kstarbound.defs.world.Biome
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
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.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.util.random.staticRandomInt
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_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.ChunkState
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
@ -62,27 +61,13 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
/** override var state: ChunkState = ChunkState.FRESH
* Determines the state chunk is in, chunks are written to persistent storage
* only if they are in [FULL] state to avoid partially loaded or partially generated
* chunks from making its way into persistent storage.
*/
enum class State {
FRESH, // Nothing is loaded
TERRAIN,
MICRO_DUNGEONS,
CAVE_LIQUID,
FULL;
}
var state: State = State.FRESH
private set private set
private var isBusy = false private var isBusy = false
private var idleTicks = 0 private var idleTicks = 0
private var ticks = 0 private var ticks = 0
private val targetState = Channel<State>(Int.MAX_VALUE) private val targetState = Channel<ChunkState>(Int.MAX_VALUE)
private val permanent = ArrayList<Ticket>() private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>() private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var nextTicketID = 0 private var nextTicketID = 0
@ -90,14 +75,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// BUT, front-end ticket creation in ServerWorld is expected to be called only on ServerWorld's thread // BUT, front-end ticket creation in ServerWorld is expected to be called only on ServerWorld's thread
// because ChunkMap is not thread-safe // because ChunkMap is not thread-safe
private val ticketsLock = ReentrantLock() private val ticketsLock = ReentrantLock()
private val loadJob = world.scope.launch { loadChunk() } private val loadJob = world.eventLoop.scope.launch { loadChunk() }
var isUnloaded = false var isUnloaded = false
private set private set
private suspend fun chunkGeneratorLoop() { private suspend fun chunkGeneratorLoop() {
while (true) { while (true) {
if (state == State.FULL) if (state == ChunkState.FULL)
break break
val targetState = targetState.receive() val targetState = targetState.receive()
@ -105,10 +90,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
while (state < targetState) { while (state < targetState) {
isBusy = true isBusy = true
val nextState = State.entries[state.ordinal + 1] val nextState = ChunkState.entries[state.ordinal + 1]
try { try {
if (nextState >= State.MICRO_DUNGEONS) { if (nextState >= ChunkState.MICRO_DUNGEONS) {
val neighbours = ArrayList<ITicket>() val neighbours = ArrayList<ITicket>()
try { try {
@ -159,8 +144,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
when (nextState) { when (nextState) {
State.TERRAIN -> { ChunkState.TERRAIN -> {
if (world.template.worldLayout == null) { if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) {
// skip since no cells will be generated anyway // skip since no cells will be generated anyway
cells.value.fill(AbstractCell.EMPTY) cells.value.fill(AbstractCell.EMPTY)
} else { } else {
@ -169,30 +154,31 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
State.MICRO_DUNGEONS -> { ChunkState.MICRO_DUNGEONS -> {
// skip if we have no layout // skip if we have no layout
if (world.template.worldLayout != null) { if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
placeMicroDungeons() placeMicroDungeons()
} }
} }
State.CAVE_LIQUID -> { ChunkState.CAVE_LIQUID -> {
// skip if we have no layout // skip if we have no layout
if (world.template.worldLayout != null) { if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
generateLiquid() generateLiquid()
} }
} }
State.FULL -> { ChunkState.FULL -> {
CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await() CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await()
// skip if we have no layout // skip if we have no layout
if (world.template.worldLayout != null) { if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
placeGrass() placeGrass()
} }
} }
State.FRESH -> throw RuntimeException() ChunkState.FRESH -> throw RuntimeException()
ChunkState.EMPTY -> {} // do nothing
} }
bumpState(nextState) bumpState(nextState)
@ -217,7 +203,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
loadCells(cells.value) loadCells(cells.value)
// bumping state while loading chunk might have // bumping state while loading chunk might have
// undesired consequences, such as if chunk requester // undesired consequences, such as if chunk requester
// is pessimistic and want "fully loaded chunk or chunk generated least X stage" // is pessimistic and want "fully loaded chunk or chunk generated to at least X stage"
// bumpState(State.CAVE_LIQUID) // bumpState(State.CAVE_LIQUID)
world.storage.loadEntities(pos).await().ifPresent { world.storage.loadEntities(pos).await().ifPresent {
@ -226,10 +212,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
bumpState(State.FULL) bumpState(ChunkState.FULL)
isBusy = false isBusy = false
return return
} else { } else {
bumpState(ChunkState.EMPTY)
// generate. // generate.
chunkGeneratorLoop() chunkGeneratorLoop()
} }
@ -244,13 +231,13 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
fun permanentTicket(target: State = State.FULL): ITicket { fun permanentTicket(target: ChunkState = ChunkState.FULL): ITicket {
ticketsLock.withLock { ticketsLock.withLock {
return Ticket(target) return Ticket(target)
} }
} }
fun temporaryTicket(time: Int, target: State = State.FULL): ITimedTicket { fun temporaryTicket(time: Int, target: ChunkState = ChunkState.FULL): ITimedTicket {
require(time >= 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
ticketsLock.withLock { ticketsLock.withLock {
@ -258,6 +245,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
if (chunkState > state) return false
return setCell(x, y, cell)
}
interface ITicket { interface ITicket {
fun cancel() fun cancel()
val isCanceled: Boolean val isCanceled: Boolean
@ -306,7 +298,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
temporary.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) } temporary.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
} }
private abstract inner class AbstractTicket(val targetState: State) : ITicket { private abstract inner class AbstractTicket(val targetState: ChunkState) : ITicket {
final override val id: Int = nextTicketID++ final override val id: Int = nextTicketID++
final override val pos: ChunkPos final override val pos: ChunkPos
get() = this@ServerChunk.pos get() = this@ServerChunk.pos
@ -336,7 +328,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
final override var listener: IChunkListener? = null final override var listener: IChunkListener? = null
} }
private inner class Ticket(state: State) : AbstractTicket(state) { private inner class Ticket(state: ChunkState) : AbstractTicket(state) {
init { init {
permanent.add(this) permanent.add(this)
@ -348,7 +340,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
private inner class TimedTicket(expiresAt: Int, state: State) : AbstractTicket(state), ITimedTicket { private inner class TimedTicket(expiresAt: Int, state: ChunkState) : AbstractTicket(state), ITimedTicket {
var expiresAt = expiresAt + ticks var expiresAt = expiresAt + ticks
override val timeRemaining: Int override val timeRemaining: Int
@ -376,7 +368,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
private fun bumpState(newState: State) { private fun bumpState(newState: ChunkState) {
if (newState == state) return if (newState == state) return
require(newState >= state) { "Tried to downgrade $this state from $state to $newState" } require(newState >= state) { "Tried to downgrade $this state from $state to $newState" }
this.state = newState this.state = newState
@ -515,24 +507,24 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
var shouldUnload = !isBusy && temporary.isEmpty() && permanent.isEmpty() var shouldUnload = !isBusy && temporary.isEmpty() && permanent.isEmpty() && world.template.worldParameters !is FloatingDungeonWorldParameters
if (shouldUnload) { if (shouldUnload) {
idleTicks++ idleTicks++
// don't load-save-load-save too frequently // don't load-save-load-save too frequently
// also make partially-generated chunks stay in memory for way longer, because re-generating // also make partially-generated chunks stay in memory for way longer, because re-generating
// them is very costly operation // them is very costly operation
shouldUnload = if (state == State.FULL) idleTicks > 600 else idleTicks > 32000 shouldUnload = if (state == ChunkState.FULL) idleTicks > 600 else idleTicks > 32000
} else { } else {
idleTicks = 0 idleTicks = 0
} }
if (shouldUnload) { if (shouldUnload) {
unload() // unload()
return return
} }
if (state != State.FULL) if (state != ChunkState.FULL)
return return
super.tick() super.tick()
@ -566,18 +558,18 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
loadJob.cancel() loadJob.cancel()
targetState.close() targetState.close()
if (state == State.FULL) { if (state == ChunkState.FULL) {
val unloadable = world.entityIndex val unloadable = world.entityIndex
.query( .query(
aabb, aabb,
filter = Predicate { it.isApplicableForUnloading && aabbd.isInside(it.position) }, filter = Predicate { it.isApplicableForUnloading && !it.isRemote && aabbd.isInside(it.position) },
distinct = true, withEdges = false) distinct = true, withEdges = false)
world.storage.saveCells(pos, copyCells()) world.storage.saveCells(pos, copyCells())
world.storage.saveEntities(pos, unloadable) world.storage.saveEntities(pos, unloadable)
unloadable.forEach { unloadable.forEach {
it.remove() it.remove(AbstractEntity.RemovalReason.UNLOADED)
} }
} }
@ -692,17 +684,17 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// TODO: Maybe somehow expand this biome meta material list? // TODO: Maybe somehow expand this biome meta material list?
// that's only 6 meta blocks in total! // that's only 6 meta blocks in total!
when (tile.material) { val indexOf = BuiltinMetaMaterials.BIOME_META_MATERIALS.indexOf(tile.material)
BuiltinMetaMaterials.BIOME -> tile.material = biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME1 -> tile.material = biome?.subBlocks?.getOrNull(0)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME2 -> tile.material = biome?.subBlocks?.getOrNull(1)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME3 -> tile.material = biome?.subBlocks?.getOrNull(2)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME4 -> tile.material = biome?.subBlocks?.getOrNull(3)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
BuiltinMetaMaterials.BIOME5 -> tile.material = biome?.subBlocks?.getOrNull(4)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
else -> {}
}
tile.hueShift = biome?.hueShift(tile.material) ?: 0f if (indexOf != -1) {
if (indexOf == 0) {
tile.material = biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
} else {
tile.material = biome?.subBlocks?.getOrNull(indexOf - 1)?.native?.entry ?: biome?.mainBlock?.native?.entry ?: BuiltinMetaMaterials.EMPTY
}
tile.hueShift = biome?.hueShift(tile.material) ?: tile.hueShift
}
if (biome == null && tile.modifier == BuiltinMetaMaterials.BIOME_MOD) { if (biome == null && tile.modifier == BuiltinMetaMaterials.BIOME_MOD) {
tile.modifier = BuiltinMetaMaterials.EMPTY_MOD tile.modifier = BuiltinMetaMaterials.EMPTY_MOD
@ -727,6 +719,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
private val missingDungeonNames = ObjectAVLTreeSet<String>()
private suspend fun placeMicroDungeons() { private suspend fun placeMicroDungeons() {
val placements = CompletableFuture.supplyAsync(Supplier { val placements = CompletableFuture.supplyAsync(Supplier {
val placements = ArrayList<BiomePlaceables.Placement>() val placements = ArrayList<BiomePlaceables.Placement>()
@ -755,12 +749,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val random = random(seed) val random = random(seed)
val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size)) val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size))
val def = Registries.dungeons[dungeon] if (dungeon.isEmpty) {
if (missingDungeonNames.add(dungeon.key.left()))
if (def == null) { LOGGER.error("Tried to place dungeon ${dungeon.key.left()}, but there is no such dungeon.")
LOGGER.error("Unknown dungeon type $dungeon!")
} else { } else {
val anchors = def.value.validAnchors(world) val anchors = dungeon.value!!.validAnchors(world)
if (anchors.isEmpty()) if (anchors.isEmpty())
continue continue
@ -782,8 +775,12 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
}.orElse(false) }.orElse(false)
if (!collision && anchor.canPlace(pos.x, pos.y, world)) { if (!collision && anchor.canPlace(pos.x, pos.y, world)) {
def.value.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await() try {
LOGGER.info("Placed dungeon $dungeon at $pos") dungeon.value!!.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await()
} catch (err: Throwable) {
LOGGER.error("Error while generating microdungeon ${dungeon.key.left()} at $pos", err)
}
break break
} }
} }
@ -820,8 +817,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// since they might be in process of generation, too // since they might be in process of generation, too
// (only if they are in process of generating someing significant, which modify terrain) // (only if they are in process of generating someing significant, which modify terrain)
// shouldn't be an issue though // shouldn't be an issue though
val cellAbove = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y + 1) val cellAbove = world.getCell(pos.tileX + x, pos.tileY + y + 1)
val cellBelow = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y - 1) val cellBelow = world.getCell(pos.tileX + x, pos.tileY + y - 1)
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))
@ -1093,7 +1090,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
for ((position, pressure) in drops.entries) { for ((position, pressure) in drops.entries) {
val cell = world.chunkMap.getCell(position).mutable() val cell = world.getCell(position).mutable()
cell.liquid.state = fillLiquid.entry!! cell.liquid.state = fillLiquid.entry!!
cell.liquid.level = 1.0f cell.liquid.level = 1.0f
@ -1103,7 +1100,9 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
cell.background.material = biomeBlock.entry ?: BuiltinMetaMaterials.EMPTY cell.background.material = biomeBlock.entry ?: BuiltinMetaMaterials.EMPTY
} }
world.chunkMap.setCell(position, cell.immutable()) check(world.setCell(position, cell.immutable())) {
"Failed to set cave liquid at $position to $fillLiquid"
}
} }
} }

View File

@ -4,9 +4,6 @@ import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -15,9 +12,7 @@ import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction import ru.dbotthepony.kstarbound.defs.WarpAction
import ru.dbotthepony.kstarbound.defs.WarpAlias import ru.dbotthepony.kstarbound.defs.WarpAlias
@ -38,9 +33,9 @@ import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
@ -51,10 +46,7 @@ import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.concurrent.withLock
class ServerWorld private constructor( class ServerWorld private constructor(
val server: StarboundServer, val server: StarboundServer,
@ -99,7 +91,7 @@ class ServerWorld private constructor(
clients.add(ServerWorldTracker(this, client, start)) clients.add(ServerWorldTracker(this, client, start))
//if (worldID is WorldID.Celestial) //if (worldID is WorldID.Celestial)
//Registries.dungeons["gardenmicrodungeons"]?.value?.generate(this@ServerWorld, random(), start.x.toInt(), start.y.toInt(), markSurfaceAndTerrain = false, forcePlacement = true) //Registries.dungeons["terrainfeatures"]?.value?.generate(this@ServerWorld, random(), start.x.toInt(), start.y.toInt(), markSurfaceAndTerrain = false, forcePlacement = true)
} finally { } finally {
isBusy-- isBusy--
} }
@ -150,8 +142,6 @@ class ServerWorld private constructor(
eventLoop.scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS) eventLoop.scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
} }
val scope = CoroutineScope(eventLoop.asCoroutineDispatcher() + SupervisorJob())
override fun toString(): String { override fun toString(): String {
return "Server World $worldID" return "Server World $worldID"
} }
@ -159,7 +149,7 @@ class ServerWorld private constructor(
private var idleTicks = 0 private var idleTicks = 0
private var isBusy = 0 private var isBusy = 0
override val isRemote: Boolean override val isClient: Boolean
get() = false get() = false
fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult { fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult {
@ -273,6 +263,53 @@ class ServerWorld private constructor(
try { try {
isBusy++ isBusy++
val random = random(template.seed)
var currentDungeonID = 0
// primary dungeons must be generated sequentially since they can get very large
// and to avoid dungeons colliding with each other, we must generate them first, one by one
for (dungeon in template.gatherDungeons()) {
var spawnDungeonRetries = Globals.worldServer.spawnDungeonRetries
do {
var x = dungeon.baseX
if (dungeon.xVariance != 0)
x += random.nextInt(dungeon.xVariance)
x = geometry.x.cell(x)
LOGGER.info("Trying to place dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}...")
val dungeonWorld = try {
dungeon.dungeon.value.generate(this, random(random.nextLong()), x, dungeon.baseHeight, dungeon.blendWithTerrain, dungeon.forcePlacement, dungeonID = currentDungeonID).await()
} catch (err: Throwable) {
LOGGER.error("Exception while placing dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}", err)
// continue
break
}
if (dungeonWorld.hasGenerated) {
LOGGER.info("Placed dungeon ${dungeon.dungeon.key} at $x, ${dungeon.baseHeight}")
if (dungeon.dungeon.value.metadata.protected) {
protectedDungeonIDs.add(currentDungeonID)
}
if (dungeon.dungeon.value.metadata.gravity != null) {
// TODO: set gravity here
}
if (dungeon.dungeon.value.metadata.breathable != null) {
// TODO: set "breathable" here
}
currentDungeonID++
break
}
} while (--spawnDungeonRetries > 0)
}
if (playerSpawnPosition == Vector2d.ZERO) { if (playerSpawnPosition == Vector2d.ZERO) {
playerSpawnPosition = findPlayerStart() playerSpawnPosition = findPlayerStart()
} }
@ -285,7 +322,7 @@ class ServerWorld private constructor(
// everything inside our own thread, not anywhere else // everything inside our own thread, not anywhere else
// This way, external callers can properly wait for preparations to complete // This way, external callers can properly wait for preparations to complete
fun prepare(): CompletableFuture<*> { fun prepare(): CompletableFuture<*> {
return scope.launch { prepare0() }.asCompletableFuture() return eventLoop.scope.launch { prepare0() }.asCompletableFuture()
} }
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d { private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {
@ -332,7 +369,7 @@ class ServerWorld private constructor(
// dungeon. // dungeon.
for (i in 0 until Globals.worldServer.playerStartRegionMaximumVerticalSearch) { for (i in 0 until Globals.worldServer.playerStartRegionMaximumVerticalSearch) {
if (!chunkMap.getCell(pos.x.toInt(), pos.y.toInt()).liquid.state.isEmptyLiquid) { if (!getCell(pos.x.toInt(), pos.y.toInt()).liquid.state.isEmptyLiquid) {
break break
} }
@ -380,29 +417,29 @@ class ServerWorld private constructor(
return ServerChunk(this, pos) return ServerChunk(this, pos)
} }
fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITicket? { fun permanentChunkTicket(pos: ChunkPos, target: ChunkState = ChunkState.FULL): ServerChunk.ITicket? {
return chunkMap.compute(pos)?.permanentTicket(target) return chunkMap.compute(pos)?.permanentTicket(target)
} }
fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITicket> { fun permanentChunkTicket(region: AABBi, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITicket> {
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
} }
fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITicket> { fun permanentChunkTicket(region: AABB, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITicket> {
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
} }
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITimedTicket? { fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ChunkState = ChunkState.FULL): ServerChunk.ITimedTicket? {
return chunkMap.compute(pos)?.temporaryTicket(time, target) return chunkMap.compute(pos)?.temporaryTicket(time, target)
} }
fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> { fun temporaryChunkTicket(region: AABBi, time: Int, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITimedTicket> {
require(time >= 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
} }
fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> { fun temporaryChunkTicket(region: AABB, time: Int, target: ChunkState = ChunkState.FULL): List<ServerChunk.ITimedTicket> {
require(time >= 0) { "Invalid ticket time: $time" } require(time >= 0) { "Invalid ticket time: $time" }
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull() return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()

View File

@ -41,14 +41,6 @@ import java.util.concurrent.atomic.AtomicBoolean
// couples ServerWorld and ServerConnection together, // couples ServerWorld and ServerConnection together,
// allowing ServerConnection client to track ServerWorld state // allowing ServerConnection client to track ServerWorld state
class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) { class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, playerStart: Vector2d) {
init {
LOGGER.info("Accepted ${client.alias()}")
client.worldStartAcknowledged = false
client.tracker = this
client.worldID = world.worldID
}
private var skyVersion = 0L private var skyVersion = 0L
// this is required because of dumb shit regarding flash time // this is required because of dumb shit regarding flash time
// if we network sky state on each tick then it will guarantee epilepsy attack // if we network sky state on each tick then it will guarantee epilepsy attack
@ -62,6 +54,12 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
init { init {
entityVersions.defaultReturnValue(-1L) entityVersions.defaultReturnValue(-1L)
LOGGER.info("Accepted ${client.alias()}")
client.worldStartAcknowledged = false
client.tracker = this
client.worldID = world.worldID
} }
fun send(packet: IPacket) = client.send(packet) fun send(packet: IPacket) = client.send(packet)
@ -118,11 +116,11 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
return entityVersions.containsKey(entity.entityID) return entityVersions.containsKey(entity.entityID)
} }
fun forget(entity: AbstractEntity, isDeath: Boolean = false) { fun forget(entity: AbstractEntity, reason: AbstractEntity.RemovalReason) {
val version = entityVersions.remove(entity.entityID) val version = entityVersions.remove(entity.entityID)
if (version != -1L) { if (version != -1L) {
send(EntityDestroyPacket(entity.entityID, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, isDeath)) send(EntityDestroyPacket(entity.entityID, entity.networkGroup.write(version, isLegacy = client.isLegacy).first, reason.dying))
} }
} }
@ -263,7 +261,8 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
for ((id, entity) in itr) { for ((id, entity) in itr) {
if (id in client.entityIDRange) { if (id in client.entityIDRange) {
entity.remove() // remove entities owned by that player
entity.remove(AbstractEntity.RemovalReason.REMOTE_REMOVAL)
} }
} }
} }

View File

@ -1,5 +1,10 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import java.util.PriorityQueue import java.util.PriorityQueue
import java.util.concurrent.Callable import java.util.concurrent.Callable
@ -12,7 +17,6 @@ import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier import java.util.function.Supplier
@ -29,14 +33,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
return executeAt - System.nanoTime() return executeAt - System.nanoTime()
} }
fun shouldEnqueue(): Boolean { fun shouldEnqueue(isShutdown: Boolean): Boolean {
if (executeAt <= System.nanoTime()) if (isShutdown || executeAt <= System.nanoTime())
return perform() return perform(isShutdown)
return true return true
} }
fun perform(): Boolean { fun perform(isShutdown: Boolean): Boolean {
if (repeat) { if (repeat) {
if (isFixedDelay) { if (isFixedDelay) {
// fixed delay // fixed delay
@ -67,7 +71,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
executeAt = now + timeDelay + deadlineMargin - (now - timeBefore) executeAt = now + timeDelay + deadlineMargin - (now - timeBefore)
} }
return true return !isShutdown
} else { } else {
run() run()
return false return false
@ -79,6 +83,8 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
private val eventQueue = LinkedBlockingQueue<TaskPair<*>>() private val eventQueue = LinkedBlockingQueue<TaskPair<*>>()
private val scheduledQueue = PriorityQueue<ScheduledTask<*>>() private val scheduledQueue = PriorityQueue<ScheduledTask<*>>()
val coroutines = asCoroutineDispatcher()
val scope = CoroutineScope(coroutines + SupervisorJob())
private fun nextDeadline(): Long { private fun nextDeadline(): Long {
if (isShutdown) if (isShutdown)
@ -95,7 +101,6 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
@Volatile @Volatile
private var isShutdown = false private var isShutdown = false
private var isRunning = true private var isRunning = true
private fun eventLoopIteration(): Boolean { private fun eventLoopIteration(): Boolean {
@ -122,22 +127,22 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
} }
if (scheduledQueue.isNotEmpty() && !isShutdown) { if (scheduledQueue.isNotEmpty()) {
val executed = ArrayList<ScheduledTask<*>>() val executed = ArrayList<ScheduledTask<*>>()
var lastSize: Int var lastSize: Int
do { do {
lastSize = executed.size lastSize = executed.size
while (scheduledQueue.isNotEmpty() && scheduledQueue.peek()!!.executeAt <= System.nanoTime() && !isShutdown) { while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.executeAt <= System.nanoTime())) {
executedAnything = true executedAnything = true
val poll = scheduledQueue.poll()!! val poll = scheduledQueue.poll()!!
if (poll.perform()) { if (poll.perform(isShutdown)) {
executed.add(poll) executed.add(poll)
} }
} }
} while (lastSize != executed.size && !isShutdown) } while (lastSize != executed.size)
scheduledQueue.addAll(executed) scheduledQueue.addAll(executed)
} }
@ -152,6 +157,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
if (isShutdown && isRunning) { if (isShutdown && isRunning) {
while (eventLoopIteration()) {} while (eventLoopIteration()) {}
isRunning = false isRunning = false
scope.cancel(CancellationException("EventLoop shut down"))
performShutdown() performShutdown()
} }
} }
@ -160,7 +166,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun execute(command: Runnable) { final override fun execute(command: Runnable) {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) { if (currentThread() === this) {
@ -178,7 +184,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun <T> submit(task: Callable<T>): CompletableFuture<T> { final override fun <T> submit(task: Callable<T>): CompletableFuture<T> {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) { if (currentThread() === this) {
@ -216,7 +222,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
fun isSameThread() = this === currentThread() fun isSameThread() = this === currentThread()
final override fun submit(task: Runnable): CompletableFuture<*> { final override fun submit(task: Runnable): CompletableFuture<*> {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) { if (currentThread() === this) {
@ -239,7 +245,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun <T> submit(task: Runnable, result: T): CompletableFuture<T> { final override fun <T> submit(task: Runnable, result: T): CompletableFuture<T> {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) { if (currentThread() === this) {
@ -264,14 +270,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun <T> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> { final override fun <T> invokeAll(tasks: Collection<Callable<T>>): List<Future<T>> {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
return tasks.map { submit(it) } return tasks.map { submit(it) }
} }
final override fun <T> invokeAll(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): List<Future<T>> { final override fun <T> invokeAll(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): List<Future<T>> {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
val futures = tasks.map { submit(it) } val futures = tasks.map { submit(it) }
@ -280,14 +286,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
final override fun <T> invokeAny(tasks: Collection<Callable<T>>): T { final override fun <T> invokeAny(tasks: Collection<Callable<T>>): T {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop shut down")
return submit(tasks.first()).get() return submit(tasks.first()).get()
} }
final override fun <T> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T { final override fun <T> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T {
if (isShutdown) if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down") throw RejectedExecutionException("EventLoop is shutting down")
return submit(tasks.first()).get(timeout, unit) return submit(tasks.first()).get(timeout, unit)
@ -311,6 +317,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
isRunning = false isRunning = false
scope.cancel(CancellationException("EventLoop shut down"))
performShutdown() performShutdown()
} else { } else {
// wake up thread // wake up thread
@ -323,37 +330,34 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
private fun shutdownNow0() {
while (eventQueue.isNotEmpty()) {
val remove = eventQueue.remove()
try {
remove.future.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
while (scheduledQueue.isNotEmpty()) {
val remove = scheduledQueue.remove()
try {
remove.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
isRunning = false
performShutdown()
}
final override fun shutdownNow(): List<Runnable> { final override fun shutdownNow(): List<Runnable> {
if (!isShutdown) { if (!isShutdown) {
isShutdown = true isShutdown = true
if (currentThread() === this) { if (currentThread() === this) {
shutdownNow0() while (eventQueue.isNotEmpty()) {
val remove = eventQueue.remove()
try {
remove.future.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
while (scheduledQueue.isNotEmpty()) {
val remove = scheduledQueue.remove()
try {
remove.cancel(false)
} catch (err: Throwable) {
LOGGER.warn("Caught exception while cancelling future during event loop shutdown", err)
}
}
isRunning = false
scope.cancel(CancellationException("EventLoop shut down"))
performShutdown()
} else { } else {
eventQueue.add(TaskPair(CompletableFuture()) { }) eventQueue.add(TaskPair(CompletableFuture()) { })
} }
@ -393,7 +397,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false) val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false)
execute { execute {
if (task.shouldEnqueue()) if (task.shouldEnqueue(isShutdown))
scheduledQueue.add(task) scheduledQueue.add(task)
} }
@ -404,7 +408,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val task = ScheduledTask(callable, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false) val task = ScheduledTask(callable, System.nanoTime() + TimeUnit.NANOSECONDS.convert(delay, unit), false, 0L, false)
execute { execute {
if (task.shouldEnqueue()) if (task.shouldEnqueue(isShutdown))
scheduledQueue.add(task) scheduledQueue.add(task)
} }
@ -420,7 +424,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(period, unit), false) val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(period, unit), false)
execute { execute {
if (task.shouldEnqueue()) if (task.shouldEnqueue(isShutdown))
scheduledQueue.add(task) scheduledQueue.add(task)
} }
@ -436,7 +440,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(delay, unit), true) val task = ScheduledTask({ command.run() }, System.nanoTime() + TimeUnit.NANOSECONDS.convert(initialDelay, unit), true, TimeUnit.NANOSECONDS.convert(delay, unit), true)
execute { execute {
if (task.shouldEnqueue()) if (task.shouldEnqueue(isShutdown))
scheduledQueue.add(task) scheduledQueue.add(task)
} }

View File

@ -0,0 +1,58 @@
package ru.dbotthepony.kstarbound.util
import java.util.concurrent.Callable
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.Delayed
import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
class ExecutorWithScheduler(val executor: ExecutorService, val scheduler: ScheduledExecutorService) : ExecutorService by executor, ScheduledExecutorService {
override fun schedule(command: Runnable, delay: Long, unit: TimeUnit): ScheduledFuture<*> {
return scheduler.schedule(Runnable {
executor.submit(command)
}, delay, unit)
}
private class Proxy<T>(val future: CompletableFuture<T>, val parent: ScheduledFuture<*>) : Future<T> by future, ScheduledFuture<T> {
override fun compareTo(other: Delayed?): Int {
return parent.compareTo(other)
}
override fun getDelay(unit: TimeUnit): Long {
return parent.getDelay(unit)
}
}
// won't react to cancels... man.
override fun <V : Any?> schedule(callable: Callable<V>, delay: Long, unit: TimeUnit): ScheduledFuture<V> {
val future = CompletableFuture<CompletionStage<V>>()
val scheduled = scheduler.schedule(Callable { future.complete(CompletableFuture.supplyAsync(callable::call, executor)) }, delay, unit)
return Proxy(future.thenCompose { it }, scheduled)
}
override fun scheduleAtFixedRate(
command: Runnable,
initialDelay: Long,
period: Long,
unit: TimeUnit
): ScheduledFuture<*> {
return scheduler.scheduleAtFixedRate(Runnable {
executor.submit(command)
}, initialDelay, period, unit)
}
override fun scheduleWithFixedDelay(
command: Runnable,
initialDelay: Long,
delay: Long,
unit: TimeUnit
): ScheduledFuture<*> {
return scheduler.scheduleWithFixedDelay(Runnable {
executor.submit(command)
}, initialDelay, delay, unit)
}
}

View File

@ -6,6 +6,7 @@ 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.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
@ -42,6 +43,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
abstract val state: ChunkState
val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE) val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE)
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE) val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE)
@ -88,11 +91,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return cells.value[x, y] return cells.value[x, y]
} }
override fun getCellDirect(x: Int, y: Int): AbstractCell { final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
return getCell(x, y)
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
val new = cell.immutable() val new = cell.immutable()

View File

@ -0,0 +1,30 @@
package ru.dbotthepony.kstarbound.world
/**
* Determines the state chunk is in, chunks are written to persistent storage
* only if they are in [FULL] state to avoid partially loaded or partially generated
* chunks from making its way into persistent storage.
*/
enum class ChunkState {
/**
* Freshly created, nothing was loaded or generated
*/
FRESH,
/**
* Chunk does not exist on disk, and awaits generation
*
* This is kind of limbo state, used by code which want to get
* chunk in its "as-is" state, be it completely empty or fully loaded from disk
*/
EMPTY,
TERRAIN,
MICRO_DUNGEONS,
CAVE_LIQUID,
/**
* Everything has been loaded or generated
*/
FULL;
}

View File

@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet 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 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.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
@ -26,8 +25,6 @@ import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.network.IPacket import ru.dbotthepony.kstarbound.network.IPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.ParallelPerform import ru.dbotthepony.kstarbound.util.ParallelPerform
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
@ -41,13 +38,11 @@ import ru.dbotthepony.kstarbound.world.physics.CollisionType
import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.Poly
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.io.Closeable
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.concurrent.withLock
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess {
@ -58,17 +53,16 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val nextEntityID = AtomicInteger() val nextEntityID = AtomicInteger()
override fun getCellDirect(x: Int, y: Int): AbstractCell {
if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL
return getCell(x, y)
}
override fun getCell(x: Int, y: Int): AbstractCell { override fun getCell(x: Int, y: Int): AbstractCell {
return chunkMap.getCell(x, y) return chunkMap.getCell(x, y)
} }
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
return chunkMap.setCell(x, y, cell) return chunkMap.setCell(x, y, cell, ChunkState.EMPTY)
}
override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
return chunkMap.setCell(x, y, cell, chunkState)
} }
protected open fun onChunkCreated(chunk: ChunkType) { } protected open fun onChunkCreated(chunk: ChunkType) { }
@ -94,11 +88,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
} }
fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { fun setCell(x: Int, y: Int, cell: AbstractCell, state: ChunkState): Boolean {
val ix = geometry.x.cell(x) val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y) val iy = geometry.y.cell(y)
val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return false val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return false
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, state)
} }
fun getCell(position: IStruct2i) = getCell(position.component1(), position.component2()) fun getCell(position: IStruct2i) = getCell(position.component1(), position.component2())
@ -214,9 +208,14 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap() val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
/**
* Random generator for in-world events
*/
val random: RandomGenerator = random() val random: RandomGenerator = random()
var gravity = Vector2d(0.0, -80.0) var gravity = Vector2d(0.0, -80.0)
abstract val isRemote: Boolean abstract val isClient: Boolean
val isServer: Boolean
get() = !isClient
// generic lock // generic lock
val lock = ReentrantLock() val lock = ReentrantLock()

View File

@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
@ -36,6 +37,16 @@ sealed class AbstractCell {
return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, blockBiome, envBiome, liquid.toLegacyNet(), dungeonId) return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, blockBiome, envBiome, liquid.toLegacyNet(), dungeonId)
} }
fun isConnectible(materialOnly: Boolean, isBackground: Boolean): Boolean {
if (isBackground && background.material.value.isConnectable)
return true
if (!isBackground && foreground.material.value.isConnectable)
return true
return !materialOnly && !isBackground && (foreground.material.value.collisionKind == CollisionType.BLOCK || foreground.material.value.collisionKind == CollisionType.PLATFORM)
}
abstract fun tile(background: Boolean): AbstractTileState abstract fun tile(background: Boolean): AbstractTileState
fun write(stream: DataOutputStream) { fun write(stream: DataOutputStream) {

View File

@ -1,7 +1,7 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.world.ChunkState
interface ICellAccess { interface ICellAccess {
/** /**
@ -10,16 +10,13 @@ interface ICellAccess {
fun getCell(x: Int, y: Int): AbstractCell fun getCell(x: Int, y: Int): AbstractCell
fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2()) fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2())
/**
* without wrap-around
*/
fun getCellDirect(x: Int, y: Int): AbstractCell
fun getCellDirect(pos: IStruct2i) = getCellDirect(pos.component1(), pos.component2())
/** /**
* whenever cell was set * whenever cell was set
*/ */
fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean
fun setCell(pos: IStruct2i, cell: AbstractCell): Boolean = setCell(pos.component1(), pos.component2(), cell) fun setCell(pos: IStruct2i, cell: AbstractCell): Boolean = setCell(pos.component1(), pos.component2(), cell)
fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean
fun setCell(pos: IStruct2i, cell: AbstractCell, chunkState: ChunkState): Boolean = setCell(pos.component1(), pos.component2(), cell, chunkState)
} }

View File

@ -9,8 +9,6 @@ interface ITileAccess : ICellAccess, CollisionTypeGetter {
// relative // relative
fun getTile(x: Int, y: Int): AbstractTileState fun getTile(x: Int, y: Int): AbstractTileState
fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2()) fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2())
fun getTileDirect(x: Int, y: Int): AbstractTileState
fun getTileDirect(pos: IStruct2i) = getTile(pos.component1(), pos.component2())
override fun collisionType(x: Int, y: Int): CollisionType { override fun collisionType(x: Int, y: Int): CollisionType {
return getTile(x, y).material.value.collisionKind return getTile(x, y).material.value.collisionKind

View File

@ -22,6 +22,22 @@ data class MutableTileState(
return this return this
} }
fun empty() {
emptyTile()
emptyModifier()
}
fun emptyTile() {
material = BuiltinMetaMaterials.EMPTY
color = TileColor.DEFAULT
hueShift = 0f
}
fun emptyModifier() {
modifier = BuiltinMetaMaterials.EMPTY_MOD
modifierHueShift = 0f
}
fun setHueShift(value: Int): MutableTileState { fun setHueShift(value: Int): MutableTileState {
if (value < 0) { if (value < 0) {
hueShift = 0f hueShift = 0f

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkState
class OffsetCellAccess(private val parent: ICellAccess, var x: Int, var y: Int) : ICellAccess { class OffsetCellAccess(private val parent: ICellAccess, var x: Int, var y: Int) : ICellAccess {
constructor(parent: ICellAccess, offset: IStruct2i) : this(parent, offset.component1(), offset.component2()) constructor(parent: ICellAccess, offset: IStruct2i) : this(parent, offset.component1(), offset.component2())
@ -10,11 +11,11 @@ class OffsetCellAccess(private val parent: ICellAccess, var x: Int, var y: Int)
return parent.getCell(x + this.x, y + this.y) return parent.getCell(x + this.x, y + this.y)
} }
override fun getCellDirect(x: Int, y: Int): AbstractCell { override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
return parent.getCellDirect(x + this.x, y + this.y) return parent.setCell(x + this.x, y + this.y, cell, chunkState)
} }
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
return parent.setCell(x, y, cell) return parent.setCell(x + this.x, y + this.y, cell)
} }
} }

View File

@ -5,19 +5,11 @@ sealed class TileView(parent: ICellAccess) : ITileAccess, ICellAccess by parent
override fun getTile(x: Int, y: Int): AbstractTileState { override fun getTile(x: Int, y: Int): AbstractTileState {
return getCell(x, y).foreground return getCell(x, y).foreground
} }
override fun getTileDirect(x: Int, y: Int): AbstractTileState {
return getCellDirect(x, y).foreground
}
} }
class Background(parent: ICellAccess) : TileView(parent) { class Background(parent: ICellAccess) : TileView(parent) {
override fun getTile(x: Int, y: Int): AbstractTileState { override fun getTile(x: Int, y: Int): AbstractTileState {
return getCell(x, y).background return getCell(x, y).background
} }
override fun getTileDirect(x: Int, y: Int): AbstractTileState {
return getCellDirect(x, y).background
}
} }
} }

View File

@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
@ -15,7 +14,6 @@ import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -65,7 +63,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
inline val clientWorld get() = world as ClientWorld inline val clientWorld get() = world as ClientWorld
inline val serverWorld get() = world as ServerWorld inline val serverWorld get() = world as ServerWorld
val isSpawned: Boolean val isInWorld: Boolean
get() = innerWorld != null get() = innerWorld != null
abstract val type: EntityType abstract val type: EntityType
@ -88,8 +86,19 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
open val isApplicableForUnloading: Boolean open val isApplicableForUnloading: Boolean
get() = true get() = true
enum class RemovalReason(val removal: Boolean, val dying: Boolean, val remote: Boolean) {
UNLOADED(false, false, false), // Being saved to disk
REMOVED(true, false, false), // Got removed from world
DYING(true, true, false), // Same as REMOVED, but indicates that entity has died
REMOTE_REMOVAL(true, false, true), // We were client for that entity,
// and other side removed that entity
REMOTE_DYING(true, true, true); // REMOTE_REMOVAL + DYING
}
protected open fun onJoinWorld(world: World<*, *>) { } protected open fun onJoinWorld(world: World<*, *>) { }
protected open fun onRemove(world: World<*, *>, isDeath: Boolean) { } protected open fun onRemove(world: World<*, *>, reason: RemovalReason) { }
val networkGroup = MasterElement(NetworkedGroup()) val networkGroup = MasterElement(NetworkedGroup())
abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean)
@ -125,20 +134,26 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
onJoinWorld(world) onJoinWorld(world)
} }
fun remove(isDeath: Boolean = false) { fun remove(reason: RemovalReason) {
val world = innerWorld ?: throw IllegalStateException("Not in world") val world = innerWorld ?: throw IllegalStateException("Not in world")
world.eventLoop.ensureSameThread() world.eventLoop.ensureSameThread()
mailbox.shutdownNow() mailbox.shutdownNow()
check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" }
onRemove(world, isDeath)
try {
onRemove(world, reason)
} catch (err: Throwable) {
LOGGER.error("Exception while removing $this from $world", err)
}
spatialEntry?.remove() spatialEntry?.remove()
spatialEntry = null spatialEntry = null
innerWorld = null innerWorld = null
if (world is ServerWorld) { if (world is ServerWorld) {
world.clients.forEach { world.clients.forEach {
it.forget(this, isDeath) it.forget(this, reason)
} }
} }
} }

View File

@ -207,7 +207,7 @@ class ActorMovementController() : MovementController() {
override fun move() { override fun move() {
// TODO: anchor entity // TODO: anchor entity
if (anchorEntity?.isSpawned != true) if (anchorEntity?.isInWorld != true)
anchorEntity = null anchorEntity = null
val anchorEntity = anchorEntity val anchorEntity = anchorEntity

View File

@ -25,6 +25,7 @@ import ru.dbotthepony.kstarbound.math.approachAngle
import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap
import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal
import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable
@ -125,7 +126,7 @@ class Animator() {
var rangeMultiplier = 0.0 var rangeMultiplier = 0.0
var soundPool by networkedList(InternedStringCodec).also { elements.add(it) } val soundPool = NetworkedList(InternedStringCodec).also { elements.add(it) }
var xPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear } var xPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
var yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear } var yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
var volumeTarget by networkedFloat(1.0).also { elements.add(it) } var volumeTarget by networkedFloat(1.0).also { elements.add(it) }
@ -474,10 +475,10 @@ class Animator() {
val sound = Sound() val sound = Sound()
if (v.isLeft) { if (v.isLeft) {
sound.soundPool = v.left() sound.soundPool.addAll(v.left())
} else { } else {
val conf = v.right() val conf = v.right()
sound.soundPool = conf.pool.map { it.fullPath } sound.soundPool.addAll(conf.pool.map { it.fullPath })
sound.xPosition = conf.position.x sound.xPosition = conf.position.x
sound.yPosition = conf.position.y sound.yPosition = conf.position.y
sound.volumeTarget = conf.volume sound.volumeTarget = conf.volume

View File

@ -36,12 +36,14 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
} }
override fun onJoinWorld(world: World<*, *>) { override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
world.dynamicEntities.add(this) world.dynamicEntities.add(this)
movement.initialize(world, spatialEntry) movement.initialize(world, spatialEntry)
forceChunkRepos = true forceChunkRepos = true
} }
override fun onRemove(world: World<*, *>, isDeath: Boolean) { override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason)
world.dynamicEntities.remove(this) world.dynamicEntities.remove(this)
movement.remove() movement.remove()
} }

View File

@ -110,8 +110,8 @@ class PlayerEntity() : HumanoidActorEntity("/") {
metaFixture = spatialEntry!!.Fixture() metaFixture = spatialEntry!!.Fixture()
} }
override fun onRemove(world: World<*, *>, isDeath: Boolean) { override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, isDeath) super.onRemove(world, reason)
metaFixture?.remove() metaFixture?.remove()
metaFixture = null metaFixture = null
} }

View File

@ -1,10 +1,21 @@
package ru.dbotthepony.kstarbound.world.entities.tile package ru.dbotthepony.kstarbound.world.entities.tile
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABB
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.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
@ -16,21 +27,16 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
protected val yTilePositionNet = networkedSignedInt() protected val yTilePositionNet = networkedSignedInt()
init { init {
xTilePositionNet.addListener(::updateSpatialIndex) xTilePositionNet.addListener(::onPositionUpdated)
yTilePositionNet.addListener(::updateSpatialIndex) yTilePositionNet.addListener(::onPositionUpdated)
} }
abstract val metaBoundingBox: AABB abstract val metaBoundingBox: AABB
protected open fun updateSpatialIndex() {
val spatialEntry = spatialEntry ?: return
spatialEntry.fixture.move(metaBoundingBox + position)
}
var xTilePosition: Int var xTilePosition: Int
get() = xTilePositionNet.get() get() = xTilePositionNet.get()
set(value) { set(value) {
if (isSpawned) { if (isInWorld) {
xTilePositionNet.accept(world.geometry.x.cell(value)) xTilePositionNet.accept(world.geometry.x.cell(value))
} else { } else {
xTilePositionNet.accept(value) xTilePositionNet.accept(value)
@ -40,7 +46,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
var yTilePosition: Int var yTilePosition: Int
get() = yTilePositionNet.get() get() = yTilePositionNet.get()
set(value) { set(value) {
if (isSpawned) { if (isInWorld) {
yTilePositionNet.accept(world.geometry.x.cell(value)) yTilePositionNet.accept(world.geometry.x.cell(value))
} else { } else {
yTilePositionNet.accept(value) yTilePositionNet.accept(value)
@ -60,19 +66,167 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
/** /**
* Tile positions this entity occupies in world (in world coordinates, not relative) * Tile positions this entity occupies in world (in world coordinates, not relative)
*/ */
abstract val occupySpaces: Set<Vector2i> abstract val occupySpaces: Collection<Vector2i>
/**
* Tile positions this entity physically occupies in world (in world coordinates, not relative)
*/
abstract val materialSpaces: Collection<Pair<Vector2i, Registry.Ref<TileDefinition>>>
/** /**
* Tile positions this entity is rooted in world (in world coordinates, not relative) * Tile positions this entity is rooted in world (in world coordinates, not relative)
*/ */
abstract val roots: Set<Vector2i> abstract val roots: Collection<Vector2i>
abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
override fun onJoinWorld(world: World<*, *>) { private var needToUpdateSpaces = false
updateSpatialIndex() private var needToUpdateRoots = false
private val currentMaterialSpaces = HashSet<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
private val currentRoots = HashSet<Vector2i>()
protected open fun markSpacesDirty() {
needToUpdateSpaces = true
} }
override fun onRemove(world: World<*, *>, isDeath: Boolean) { protected open fun markRootsDirty() {
needToUpdateRoots = true
}
private fun updateSpatialPosition() {
val spatialEntry = spatialEntry ?: return
spatialEntry.fixture.move(metaBoundingBox + position)
}
protected open fun onPositionUpdated() {
updateSpatialPosition()
markSpacesDirty()
}
override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
updateSpatialPosition()
markSpacesDirty()
}
override fun onRemove(world: World<*, *>, reason: RemovalReason) {
super.onRemove(world, reason)
if (world.isServer && (reason.removal || reason.remote)) {
// remove occupied spaces
if (!updateMaterialSpaces(listOf())) {
// we were unable to remove all spaces...
// create a task to try not to leave world in inconsistent state!
val currentMaterialSpaces = currentMaterialSpaces
world.eventLoop.scope.launch {
world as ServerWorld
val tickets = ArrayList<ServerChunk.ITicket>()
try {
currentMaterialSpaces.forEach { (p) ->
tickets.add(world.permanentChunkTicket(ChunkPos(world.geometry.x.chunkFromCell(p.x), world.geometry.x.chunkFromCell(p.y)), ChunkState.EMPTY) ?: return@forEach)
}
tickets.forEach { it.chunk.await() }
for (space in currentMaterialSpaces) {
val cell = world.getCell(space.first).mutable()
if (cell.foreground.material == space.second.orEmptyTile)
cell.foreground.empty()
if (!world.setCell(space.first, cell)) {
LOGGER.warn("Unable to clear tile entity space at ${space.first}, world left in inconsistent state.")
}
}
} finally {
tickets.forEach { it.cancel() }
}
}
}
}
}
protected fun updateMaterialSpaces(desired: Collection<Pair<Vector2i, Registry.Ref<TileDefinition>>>): Boolean {
val toRemove = ArrayList<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
val toPlace = ArrayList<Pair<Vector2i, Registry.Ref<TileDefinition>>>()
for (space in currentMaterialSpaces) {
if (desired.any { it == space })
continue
// we need to remove this space
toRemove.add(space)
}
for (space in desired) {
if (currentMaterialSpaces.any { it == space })
continue
// we need to put this space
toPlace.add(space)
}
if (toRemove.isEmpty() && toPlace.isEmpty())
return true // we're clear!
var clear = true
for (space in toRemove) {
val cell = world.getCell(space.first).mutable()
if (cell.foreground.material == space.second.orEmptyTile)
cell.foreground.empty()
if (world.setCell(space.first, cell)) {
currentMaterialSpaces.remove(space)
} else {
clear = false
}
}
for (space in toPlace) {
val cell = world.getCell(space.first).mutable()
// already satisfied that placement
if (cell.foreground.material == space.second.orEmptyTile) {
currentMaterialSpaces.add(space)
continue
}
cell.foreground.empty()
cell.foreground.material = space.second.orEmptyTile
if (world.setCell(space.first, cell)) {
currentMaterialSpaces.add(space)
} else {
clear = false
}
}
return clear
}
fun updateMaterialSpacesNow() {
needToUpdateSpaces = false
// only server can update entity tiles
// even if this tile entity is owned by client
if (world.isServer) {
needToUpdateSpaces = !updateMaterialSpaces(materialSpaces)
}
}
override fun tick() {
super.tick()
if (needToUpdateSpaces) {
updateMaterialSpacesNow()
}
}
companion object {
private val LOGGER = LogManager.getLogger()
} }
} }

View File

@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.defs.DamageSource
import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractAction
import ru.dbotthepony.kstarbound.defs.InteractRequest import ru.dbotthepony.kstarbound.defs.InteractRequest
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
@ -79,8 +80,12 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull)) uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))
for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) { loadParameters(data.get("parameters") { JsonObject() })
parameters[k] = v }
open fun loadParameters(parameters: JsonObject) {
for ((k, v) in parameters.entrySet()) {
this.parameters[k] = v
} }
} }
@ -111,9 +116,7 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also { val parameters = NetworkedMap(InternedStringCodec, JsonElementCodec).also {
networkGroup.upstream.add(it) networkGroup.upstream.add(it)
it.addListener(Runnable { it.addListener { invalidate() }
invalidate()
})
} }
val orientation: ObjectOrientation? get() { val orientation: ObjectOrientation? get() {
@ -139,17 +142,45 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) } var isInteractive by networkedBoolean().also { networkGroup.upstream.add(it) }
var materialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) } val networkedMaterialSpaces = NetworkedList(materialSpacesCodec, materialSpacesCodecLegacy).also { networkGroup.upstream.add(it) }
private val materialSpaces0 = LazyData {
networkedMaterialSpaces.map { (it.first + tilePosition) to it.second }
}
override val materialSpaces by materialSpaces0
private val occupySpaces0 = LazyData {
(orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet())
}
override val occupySpaces: ImmutableSet<Vector2i> by occupySpaces0
override val roots: Set<Vector2i>
get() = setOf()
private val anchorPositions0 = LazyData {
immutableSet {
orientation?.anchors?.forEach { accept(it.position + tilePosition) }
}
}
val anchorPositions: ImmutableSet<Vector2i> by anchorPositions0
init { init {
networkGroup.upstream.add(xTilePositionNet) networkGroup.upstream.add(xTilePositionNet)
networkGroup.upstream.add(yTilePositionNet) networkGroup.upstream.add(yTilePositionNet)
networkedMaterialSpaces.addListener {
materialSpaces0.invalidate()
markSpacesDirty()
}
} }
var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) } var direction by networkedEnum(Direction.LEFT).also { networkGroup.upstream.add(it) }
var health by networkedFloat().also { networkGroup.upstream.add(it) } var health by networkedFloat().also { networkGroup.upstream.add(it) }
var orientationIndex by networkedPointer().also { private var orientationIndex by networkedPointer(-1L).also {
networkGroup.upstream.add(it) networkGroup.upstream.add(it)
it.addListener(Runnable { invalidate() }) it.addListener(Runnable { invalidate() })
} }
@ -194,10 +225,16 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
init { init {
if (config.value.animation?.value != null) { if (config.value.animation?.value != null) {
animator = Animator(config.value.animation!!.value!!).also { networkGroup.upstream.add(it.networkGroup) } if (config.value.animationCustom.size() > 0 && config.value.animation!!.json != null) {
animator = Animator(Starbound.gson.fromJson(mergeJson(config.value.animation!!.json!!, config.value.animationCustom), AnimationDefinition::class.java))
} else {
animator = Animator(config.value.animation!!.value!!)
}
} else { } else {
animator = Animator().also { networkGroup.upstream.add(it.networkGroup) } animator = Animator()
} }
networkGroup.upstream.add(animator.networkGroup)
} }
val unbreakable by LazyData { val unbreakable by LazyData {
@ -246,35 +283,79 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
init { init {
networkedRenderKeys.addListener(Runnable { drawablesCache.invalidate() }) networkedRenderKeys.addListener { drawablesCache.invalidate() }
} }
val drawables: List<Drawable> by drawablesCache val drawables: List<Drawable> by drawablesCache
private val occupySpaces0 = LazyData { private fun updateOrientation() {
(orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet()) setOrientation(config.value.findValidOrientation(world, tilePosition, direction))
} }
override val occupySpaces: ImmutableSet<Vector2i> by occupySpaces0 fun setOrientation(index: Int) {
if (orientationIndex.toInt() == index)
return
override val roots: Set<Vector2i> orientationIndex = index.toLong()
get() = setOf()
private val anchorPositions0 = LazyData { val orientation = orientation
immutableSet {
orientation?.anchors?.forEach { accept(it.pos + tilePosition) } if (orientation != null) {
if (orientation.directionAffinity != null) {
direction = orientation.directionAffinity
}
networkedMaterialSpaces.clear()
networkedMaterialSpaces.addAll(orientation.materialSpaces)
} else {
networkedMaterialSpaces.clear()
} }
} }
val anchorPositions: ImmutableSet<Vector2i> by anchorPositions0 override fun onPositionUpdated() {
super.onPositionUpdated()
override fun updateSpatialIndex() {
super.updateSpatialIndex()
occupySpaces0.invalidate() occupySpaces0.invalidate()
anchorPositions0.invalidate() anchorPositions0.invalidate()
materialSpaces0.invalidate()
if (isInWorld && world.isServer) {
updateMaterialSpaces(listOf()) // remove old spaces after moving before updating orientation
// update orientation
updateOrientation()
// reapply world spaces
updateMaterialSpacesNow()
}
} }
fun getRenderParam(key: String): String? { override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
val orientation = orientation
if (orientation == null)
updateOrientation() // try to find valid orientation
else {
if (orientation.directionAffinity != null) {
direction = orientation.directionAffinity
}
networkedMaterialSpaces.clear()
networkedMaterialSpaces.addAll(orientation.materialSpaces)
}
setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString)
for ((k, v) in lookupProperty(JsonPath("animationParts")) { JsonObject() }.asJsonObject.entrySet()) {
animator.setPartTag(k, "partImage", v.asString)
}
updateMaterialSpacesNow()
}
fun getRenderParam(key: String): String {
return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default" return localRenderKeys[key] ?: networkedRenderKeys[key] ?: "default"
} }
@ -300,7 +381,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
override fun interact(request: InteractRequest): InteractAction { override fun interact(request: InteractRequest): InteractAction {
val diff = world.geometry.diff(request.sourcePos, position) val diff = world.geometry.diff(request.sourcePos, position)
// val result =
if (!interactAction.isJsonNull) { if (!interactAction.isJsonNull) {
return InteractAction(interactAction.asString, entityID, interactData) return InteractAction(interactAction.asString, entityID, interactData)
@ -337,11 +417,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
override fun onJoinWorld(world: World<*, *>) {
super.onJoinWorld(world)
setImageKey("color", lookupProperty(JsonPath("color")) { JsonPrimitive("default") }.asString)
}
override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean { override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
if (unbreakable) if (unbreakable)
return false return false
@ -415,5 +490,21 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
result.deserialize(content) result.deserialize(content)
return result return result
} }
fun create(prototype: Registry.Entry<ObjectDefinition>, position: Vector2i, parameters: JsonObject = JsonObject()): WorldObject? {
val result = when (prototype.value.objectType) {
ObjectType.OBJECT -> WorldObject(prototype)
ObjectType.LOUNGEABLE -> LoungeableObject(prototype)
ObjectType.CONTAINER -> ContainerObject(prototype)
//ObjectType.FARMABLE -> TODO("ObjectType.FARMABLE")
//ObjectType.TELEPORTER -> TODO("ObjectType.TELEPORTER")
//ObjectType.PHYSICS -> TODO("ObjectType.PHYSICS")
else -> null
}
result?.loadParameters(parameters)
result?.tilePosition = position
return result
}
} }
} }