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
* 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)
---------------
### 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
kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.12.3
kommonsVersion=2.13.1
ffiVersion=2.2.13
lwjglVersion=3.3.0

View File

@ -106,7 +106,8 @@ object Registries {
private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>,
files: List<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, Int?>
noinline keyProvider: (T) -> Pair<String, Int?>,
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) }
@ -120,6 +121,8 @@ object Registries {
val read = adapter.fromJsonTree(elem)
val keys = keyProvider(read)
after(read, listedFile)
registry.add {
if (keys.second != null)
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 it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager
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.NativeLegacy
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutorWithScheduler
import ru.dbotthepony.kstarbound.util.Directives
import ru.dbotthepony.kstarbound.util.ExceptionLogger
import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.HashTableInterner
import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.*
@ -138,7 +136,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
@JvmField
val EXECUTOR: ForkJoinPool = ForkJoinPool.commonPool()
@JvmField
val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher()
val COROUTINE_EXECUTOR = ExecutorWithScheduler(EXECUTOR, this).asCoroutineDispatcher()
@JvmField
val CLEANER: Cleaner = Cleaner.create {
@ -152,7 +150,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
// Hrm.
// val strings: Interner<String> = Interner.newWeakInterner()
// val strings: Interner<String> = Interner { it }
@JvmField
@JvmField
val STRINGS: Interner<String> = interner(5)
// 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.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.formatBytesShort
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.RayDirection
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)
}
override fun getCellDirect(x: Int, y: Int): AbstractCell {
return world!!.getCellDirect(x + viewportCellX, y + viewportCellY)
override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
return world!!.setCell(x + viewportCellX, y + viewportCellY, cell, chunkState)
}
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.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.api.AbstractCell
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) {
super.foregroundChanges(x, y, cell)

View File

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

View File

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

View File

@ -7,4 +7,5 @@ class WorldServerConfig(
val playerStartRegionMaximumTries: Int = 1,
val playerStartRegionMaximumVerticalSearch: Int = 1,
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.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
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.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.CompletableFuture
import java.util.random.RandomGenerator
@ -41,7 +43,7 @@ data class DungeonDefinition(
val metadata: Metadata,
// relevant for PNG defined dungeons
val tiles: ImageTileSet = ImageTileSet(),
val parts: ImmutableList<DungeonPart>,
private val parts: ImmutableList<JsonObject>,
) {
@JsonFactory
data class Metadata(
@ -67,22 +69,39 @@ data class DungeonDefinition(
get() = metadata.name
init {
parts.forEach { it.bind(this) }
tiles.spewWarnings(name)
}
for (anchor in metadata.anchor) {
if (!parts.any { it.name == anchor }) {
throw JsonSyntaxException("Dungeon contains $anchor as anchor, but there is no such part")
private val directory = AssetPathStack.last()
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> {
val result = ArrayList<DungeonPart.JigsawConnector>()
for (part in parts) {
for (part in actualParts) {
if (!part.doesNotConnectTo(connector.part)) {
for (pconnector in part.connectors) {
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> {
@ -201,7 +220,7 @@ data class DungeonDefinition(
return CoroutineScope(Starbound.COROUTINE_EXECUTOR)
.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)
if (commit) {

View File

@ -189,7 +189,7 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null)
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))
return false
@ -198,7 +198,7 @@ abstract class DungeonRule {
}
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
}
@ -215,12 +215,12 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null)
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)
}
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
}
@ -237,7 +237,7 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null)
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))
return false
@ -246,7 +246,7 @@ abstract class DungeonRule {
}
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
}
@ -263,12 +263,12 @@ abstract class DungeonRule {
if (world.markSurfaceLevel != null)
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)
}
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
}

View File

@ -69,7 +69,7 @@ data class DungeonTile(
// but thats also not a priority, since this check happens quite quickly
// to have any noticeable impact on world's performance
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)
return false
@ -81,7 +81,7 @@ data class DungeonTile(
}
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)
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.Long2ObjectOpenHashMap
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
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.TileModifierDefinition
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.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.util.Collections
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
import java.util.random.RandomGenerator
@ -63,18 +68,28 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
val parameters: JsonObject = JsonObject()
)
private val liquid = HashMap<Vector2i, AbstractLiquidState>()
private val foregroundMaterial = HashMap<Vector2i, Material>()
private val foregroundModifier = HashMap<Vector2i, Modifier>()
private val backgroundMaterial = HashMap<Vector2i, Material>()
private val backgroundModifier = HashMap<Vector2i, Modifier>()
var hasGenerated = false
private set
val targetChunkState = if (parent.template.worldParameters is FloatingDungeonWorldParameters) ChunkState.FULL else ChunkState.TERRAIN
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
// are occupied by tile entity
private val clearTileEntitiesAt = HashSet<Vector2i>()
private val clearTileEntitiesAt = HashSet<Vector2i>(8192, 0.5f)
// 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) {
clearTileEntitiesAt.add(geometry.wrap(Vector2i(x, y)))
@ -92,11 +107,6 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
tileEntitiesToRemove.add(entity)
}
private val touchedTiles = HashSet<Vector2i>()
private val protectTile = HashSet<Vector2i>()
private val boundingBoxes = ArrayList<AABBi>()
private var currentBoundingBox: AABBi? = null
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)))
}
private val biomeItems = HashSet<Vector2i>()
private val biomeItems = HashSet<Vector2i>(8192, 0.5f)
private val biomeTrees = HashSet<Vector2i>()
private val itemDrops = 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 val pendingLiquids = HashMap<Vector2i, AbstractLiquidState>()
private val pendingLiquids = HashMap<Vector2i, AbstractLiquidState>(8192, 0.5f)
private val openLocalWires = 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>()
return try {
tickets.addAll(parent.permanentChunkTicket(region, ServerChunk.State.TERRAIN))
tickets.addAll(parent.permanentChunkTicket(region, targetChunkState))
tickets.forEach { it.chunk.await() }
block()
} finally {
@ -290,7 +300,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
return waitForRegionAndJoin(AABBi(position, position + size), block)
}
fun pressurizeLiquids() {
fun applyFinalTouches() {
// For each liquid type, find each contiguous region of liquid, then
// 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()
hasGenerated = true
}
private fun applyCellChangesAt(pos: Vector2i, chunk: ServerChunk) {
@ -422,64 +434,95 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
}.await()
for (box in boundingBoxes) {
tickets.addAll(parent.permanentChunkTicket(box, ServerChunk.State.TERRAIN))
tickets.addAll(parent.permanentChunkTicket(box, targetChunkState))
}
// apply tiles to world per-chunk
// this way we don't need to wait on all chunks to be loaded
// 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)
tilePositionsRaw.addAll(foregroundModifier.keys)
tilePositionsRaw.addAll(backgroundMaterial.keys)
tilePositionsRaw.addAll(backgroundModifier.keys)
tilePositionsRaw.sortWith { o1, o2 ->
val cmp = o1.x.compareTo(o2.x)
if (cmp == 0) o1.y.compareTo(o2.y) else cmp
}
tilePositions.addAll(foregroundMaterial.keys)
tilePositions.addAll(foregroundModifier.keys)
tilePositions.addAll(backgroundMaterial.keys)
tilePositions.addAll(backgroundModifier.keys)
tilePositions.addAll(liquid.keys)
val regions = Long2ObjectOpenHashMap<ArrayList<Vector2i>>()
var previous: Vector2i? = null
for (pos in tilePositionsRaw) {
if (pos != previous) {
regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos)
previous = pos
}
for (pos in tilePositions) {
regions.computeIfAbsent(ChunkPos.toLong(geometry.x.chunkFromCell(pos.x), geometry.y.chunkFromCell(pos.y)), Long2ObjectFunction { ArrayList() }).add(pos)
}
val seenTickets = HashSet<ChunkPos>()
val waiters = ArrayList<CompletableFuture<*>>()
for (ticket in tickets.filter { seenTickets.add(it.pos) }) {
// make changes to chunk only inside world's thread once it has reached TILES state
ticket.chunk.thenAcceptAsync(Consumer {
regions.get(ticket.pos.toLong())?.forEach { applyCellChangesAt(it, ticket.chunk.get()) }
}, parent.eventLoop)
waiters.add(ticket.chunk.thenAcceptAsync(Consumer {
regions.get(ticket.pos.toLong())?.forEach {
applyCellChangesAt(it, ticket.chunk.get())
}
}, parent.eventLoop))
}
// wait for all chunks to be loaded
tickets.forEach { it.chunk.await() }
// wait for all chunks to be loaded (and cell changes to be applied)
// 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
if (playerStart != null)
parent.setPlayerSpawn(playerStart!!, false)
// and finally, schedule chunks to be loaded into FULL state
// this way, big dungeons won't get cut off when chunks being saved
// to disk because of multiple chunks outside player tracking area
// But this might trigger cascading world generation
// (big dungeon generates another big 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, ServerChunk.State.FULL)
val placedObjects = placedObjects.entries.stream()
.sorted { o1, o2 -> o1.key.y.compareTo(o2.key.y) } // place objects from bottom to top
// so objects stacked on each other can be properly placed
.map { (pos, data) ->
WorldObject.create(data.prototype, pos, data.parameters) to data.direction
}
.filter { it.first != null }
.toList()
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 {
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)
}
}

View File

@ -37,7 +37,8 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList<Image>) : Par
for (y in 0 until image.height) {
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 + 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) {
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 + 2].toInt().and(0xFF).shl(16) or
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> {
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 (x in 0 until layer.data.width) {
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 ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kstarbound.json.getAdapter
import java.util.LinkedList
// dungeons are stored as images, and each pixel
// represents a different tile. To make sense
@ -19,6 +20,8 @@ import ru.dbotthepony.kstarbound.json.getAdapter
class ImageTileSet(list: List<DungeonTile> = listOf()) {
private val mapping = Int2ObjectOpenHashMap<DungeonTile>()
private val warnings = LinkedList<String>()
init {
for ((i, it) in list.withIndex()) {
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.
if (replaced != null && replaced != it) {
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? {
return mapping.get(index)
}

View File

@ -225,8 +225,9 @@ class TiledMap(data: JsonData) : TileMap() {
}
override fun <T> walkTiles(callback: TileCallback<T>): KOptional<T> {
for (x in this.x until this.x + width) {
for (y in this.y until this.y + height) {
// 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 (x in this.x until this.x + width) {
val result = callback(x, y, get0(x, y))
if (result.isPresent) return result
}
@ -418,8 +419,8 @@ class TiledMap(data: JsonData) : TileMap() {
ObjType.RECTANGLE -> {
// 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)
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.gson.JsonObject
import com.google.gson.JsonSyntaxException
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
@ -15,13 +16,15 @@ import ru.dbotthepony.kstarbound.set
import java.util.concurrent.ConcurrentHashMap
class TiledTileSet private constructor(
val front: ImmutableMap<Int, Pair<DungeonTile, JsonObject>>,
val back: ImmutableMap<Int, Pair<DungeonTile, JsonObject>>,
// plz dont modify :)
// used for speed
val front: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>,
val back: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>,
) {
@JsonFactory
data class JsonData(
val properties: JsonObject = JsonObject(),
// val tilecount: Int, // we don't care
val tilecount: Int,
val tileproperties: JsonObject = JsonObject(), // empty tileset?
)
@ -59,8 +62,8 @@ class TiledTileSet private constructor(
try {
val data = Starbound.gson.fromJson(locate, JsonData::class.java)
val front = ImmutableMap.Builder<Int, Pair<DungeonTile, JsonObject>>()
val back = ImmutableMap.Builder<Int, Pair<DungeonTile, JsonObject>>()
val front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(data.tilecount + 40)
val back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>(data.tilecount + 40)
for ((key, value) in data.tileproperties.entrySet()) {
if (value !is JsonObject)
@ -88,7 +91,7 @@ class TiledTileSet private constructor(
back[index] = makeTile(mergeBack) to mergeBack
}
return Either.left(TiledTileSet(front.build(), back.build()))
return Either.left(TiledTileSet(front, back))
} catch (err: Throwable) {
return Either.right(err)
}

View File

@ -14,11 +14,11 @@ class TiledTileSets(entries: List<Entry>) {
val source: String,
)
private var front = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>()
private var back = Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>()
private var front: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>
private var back: Int2ObjectOpenHashMap<Pair<DungeonTile, JsonObject>>
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
// 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
@ -36,11 +36,16 @@ class TiledTileSets(entries: List<Entry>) {
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) {
front[firstgid + i] = set.front[i] ?: throw NullPointerException("aeiou")
back[firstgid + i] = set.back[i] ?: throw NullPointerException("aeiou")
front.put(firstgid + i, set.front[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`
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.getArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
@JsonAdapter(ObjectDefinition.Adapter::class)
data class ObjectDefinition(
@ -51,7 +54,7 @@ data class ObjectDefinition(
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
val animation: AssetReference<AnimationDefinition>? = null,
//val animation: AssetPath? = null,
val animationCustom: JsonObject = JsonObject(),
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
val smashParticles: JsonArray? = null,
val smashable: Boolean = false,
@ -81,6 +84,28 @@ data class ObjectDefinition(
val flickerPeriod: PeriodicFunction? = null,
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>() {
@JsonFactory(logMisses = false)
data class PlainData(
@ -100,6 +125,7 @@ data class ObjectDefinition(
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
val animation: AssetReference<AnimationDefinition>? = null,
val animationCustom: JsonObject = JsonObject(),
//val animation: AssetPath? = null,
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
val smashParticles: JsonArray? = null,
@ -203,6 +229,7 @@ data class ObjectDefinition(
smashDropPool = basic.smashDropPool,
smashDropOptions = basic.smashDropOptions,
animation = basic.animation,
animationCustom = basic.animationCustom,
smashSounds = basic.smashSounds,
smashParticles = basic.smashParticles,
smashable = basic.smashable,

View File

@ -31,7 +31,11 @@ import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
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.World
import kotlin.math.PI
@JsonAdapter(ObjectOrientation.Adapter::class)
@ -49,7 +53,7 @@ data class ObjectOrientation(
val metaBoundBox: AABB?,
val anchors: ImmutableSet<Anchor>,
val anchorAny: Boolean,
val directionAffinity: Side?,
val directionAffinity: Direction?,
val materialSpaces: ImmutableList<Pair<Vector2i, Registry.Ref<TileDefinition>>>,
val interactiveSpaces: ImmutableSet<Vector2i>,
val lightPosition: Vector2i,
@ -58,6 +62,38 @@ data class ObjectOrientation(
val touchDamage: JsonReference.Object?,
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 {
fun preprocess(json: JsonArray): JsonArray {
val actual = ArrayList<JsonObject>()
@ -197,16 +233,16 @@ data class ObjectOrientation(
val metaBoundBox = obj["metaBoundBox"]?.let { aabbs.fromJsonTree(it) }
val requireTilledAnchors = obj.get("requireTilledAnchors", 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>()
for (v in obj.get("anchors", JsonArray())) {
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)) }
"right" -> occupySpaces.stream().filter { it.x == boundingBox.maxs.x }.forEach { anchors.add(Anchor(true, 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)) }
"bottom" -> occupySpaces.stream().filter { it.y == boundingBox.maxs.y }.forEach { anchors.add(Anchor(true, it + Vector2i.NEGATIVE_Y, requireTilledAnchors, requireSoilAnchors, anchorMaterial)) }
"background" -> occupySpaces.forEach { anchors.add(Anchor(false, it + Vector2i.NEGATIVE_Y, 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(false, it + Vector2i.POSITIVE_X, 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.mins.y }.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")
}
}
@ -218,7 +254,7 @@ data class ObjectOrientation(
anchors.add(Anchor(true, vectorsi.fromJsonTree(v), requireTilledAnchors, requireSoilAnchors, anchorMaterial))
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>>
if ("materialSpaces" in obj) {
@ -227,7 +263,7 @@ data class ObjectOrientation(
val collisionSpaces = obj["collisionSpaces"]?.let { this.spaces.fromJsonTree(it) } ?: occupySpaces
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) }
"platform" -> collisionSpaces.forEach { if (it.y == boundingBox.maxs.y) builder.add(it to BuiltinMetaMaterials.OBJECT_PLATFORM.key) }
"none" -> {}

View File

@ -101,13 +101,14 @@ const val PROTECTED_ZERO_GRAVITY_DUNGEON_ID = 65524
const val FIRST_RESERVED_DUNGEON_ID = 65520
object BuiltinMetaMaterials {
private fun make(id: Int, name: String, collisionType: CollisionType) = Registries.tiles.add(name, id, TileDefinition(
private fun make(id: Int, name: String, collisionType: CollisionType, isConnectable: Boolean = true) = Registries.tiles.add(name, id, TileDefinition(
materialId = id,
materialName = "metamaterial:$name",
descriptionData = ThingDescription.EMPTY,
category = "meta",
renderTemplate = AssetReference.empty(),
renderParameters = RenderParameters.META,
isConnectable = isConnectable,
isMeta = true,
supportsMods = false,
collisionKind = collisionType,
@ -132,12 +133,12 @@ object BuiltinMetaMaterials {
/**
* air
*/
val EMPTY = make(65535, "empty", CollisionType.NONE)
val EMPTY = make(65535, "empty", CollisionType.NONE, isConnectable = false)
/**
* 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 BIOME = make(65527, "biome", CollisionType.BLOCK)
@ -146,7 +147,7 @@ object BuiltinMetaMaterials {
val BIOME3 = make(65530, "biome3", CollisionType.BLOCK)
val BIOME4 = make(65531, "biome4", 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_PLATFORM = make(65501, "objectplatform", CollisionType.PLATFORM)

View File

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

View File

@ -18,6 +18,7 @@ data class TileModifierDefinition(
val requiredHarvestLevel: Int? = null,
val breaksWithTile: Boolean = true,
val grass: Boolean = false,
val tilled: Boolean = false,
val miningParticle: 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.value
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
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.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
@ -114,9 +116,7 @@ data class BiomePlaceables(
// Truly our hero here.
val obj = when (val type = `in`.nextString()) {
"treasureBoxSet" -> TreasureBox(`in`.nextString())
"microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { it.asString }.collect(
ImmutableSet.toImmutableSet()
))
"microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet()))
"grass" -> Grass(grassVariant.read(`in`))
"bush" -> Bush(bushVariant.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
get() = BiomePlacementItemType.MICRO_DUNGEON
override fun toJson(): JsonElement {
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.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
@ -62,7 +63,7 @@ data class BiomePlaceablesDefinition(
}
@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
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.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
@ -25,7 +27,7 @@ data class DungeonWorldsConfig(
val dungeonBaseHeight: Int,
val dungeonSurfaceHeight: Int = dungeonBaseHeight,
val dungeonUndergroundLevel: Int = 0,
val primaryDungeon: String,
val primaryDungeon: Registry.Ref<DungeonDefinition>,
val biome: String? = null,
val ambientLightLevel: RGBAColor,

View File

@ -7,7 +7,9 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.io.readColor
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readNullableString
@ -27,7 +29,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
private set
var dungeonUndergroundLevel: Int by Delegates.notNull()
private set
var primaryDungeon: String by Delegates.notNull()
var primaryDungeon: Registry.Ref<DungeonDefinition> by Delegates.notNull()
private set
var ambientLightLevel: RGBAColor by Delegates.notNull()
private set
@ -71,9 +73,10 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
@JsonFactory
data class JsonData(
val dungeonBaseHeight: Int,
val dungeonSurfaceHeight: Int,
val dungeonUndergroundLevel: Int,
val primaryDungeon: String,
val primaryDungeon: Registry.Ref<DungeonDefinition>,
val biome: String? = null,
val ambientLightLevel: RGBAColor,
val dayMusicTrack: String? = null,
@ -87,6 +90,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
val read = Starbound.gson.fromJson(data, JsonData::class.java)
dungeonBaseHeight = read.dungeonBaseHeight
dungeonSurfaceHeight = read.dungeonSurfaceHeight
dungeonUndergroundLevel = read.dungeonUndergroundLevel
primaryDungeon = read.primaryDungeon
@ -102,7 +106,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
super.toJson(data, isLegacy)
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
for ((k, v) in serialize.entrySet()) {
@ -116,7 +120,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
dungeonBaseHeight = stream.readInt()
dungeonSurfaceHeight = stream.readInt()
dungeonUndergroundLevel = stream.readInt()
primaryDungeon = stream.readInternedString()
primaryDungeon = Registries.dungeons.ref(stream.readInternedString())
biome = stream.readNullableString()
ambientLightLevel = stream.readColor()
dayMusicTrack = stream.readNullableString()
@ -131,7 +135,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
stream.writeInt(dungeonBaseHeight)
stream.writeInt(dungeonSurfaceHeight)
stream.writeInt(dungeonUndergroundLevel)
stream.writeBinaryString(primaryDungeon)
stream.writeBinaryString(primaryDungeon.key.left())
stream.writeNullableString(biome)
stream.writeColor(ambientLightLevel)
stream.writeNullableString(dayMusicTrack)
@ -145,6 +149,7 @@ class FloatingDungeonWorldParameters : VisitableWorldParameters() {
val config = Globals.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!")
val parameters = FloatingDungeonWorldParameters()
parameters.typeName = typeName
parameters.worldSize = config.worldSize
parameters.threatLevel = config.threatLevel
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.collect.WeightedList
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.LiquidDefinition
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.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.json.mergeJson
import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.readJsonElement
@ -131,7 +133,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
val layerMinHeight: Int,
val layerBaseHeight: Int,
val dungeons: ImmutableSet<String>,
val dungeons: ImmutableSet<Registry.Ref<DungeonDefinition>>,
val dungeonXVariance: Int,
val primaryRegion: Region,
@ -146,7 +148,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
constructor(stream: DataInputStream) : this(
stream.readInt(),
stream.readInt(),
ImmutableSet.copyOf(stream.readCollection { readInternedString() }),
ImmutableSet.copyOf(stream.readCollection { Registries.dungeons.ref(readInternedString()) }),
stream.readInt(),
Region(stream),
@ -163,7 +165,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
stream.writeInt(layerMinHeight)
stream.writeInt(layerBaseHeight)
stream.writeCollection(dungeons) { writeBinaryString(it) }
stream.writeCollection(dungeons) { writeBinaryString(it.key.left()) }
stream.writeInt(dungeonXVariance)
primaryRegion.write(stream)
@ -287,6 +289,18 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
var coreLayer: Layer by Delegates.notNull()
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
get() = VisitableWorldParametersType.TERRESTRIAL
@ -397,7 +411,7 @@ class TerrestrialWorldParameters : VisitableWorldParameters() {
private val biomePairs by lazy { Starbound.gson.pairAdapter<Double, JsonArray>() }
private val vectors2d by lazy { Starbound.gson.getAdapter(Vector2d::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 {
return generate(typeName, sizeName, random(seed))

View File

@ -118,6 +118,9 @@ class WorldLayout {
}
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)
if (wx < boundaries.first()) {

View File

@ -1,8 +1,8 @@
package ru.dbotthepony.kstarbound.defs.world
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.gson.JsonObject
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.Either
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.Registry
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.LiquidDefinition
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.util.random.random
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.UniversePos
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.time.Duration
import java.util.concurrent.CopyOnWriteArrayList
import java.util.random.RandomGenerator
class WorldTemplate(val geometry: WorldGeometry) {
@ -43,16 +45,20 @@ class WorldTemplate(val geometry: WorldGeometry) {
val threatLevel: Double
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)) {
this.seed = seed
this.worldParameters = worldParameters
this.skyParameters = skyParameters
this.worldLayout = worldParameters.createLayout(seed)
}
constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, random: RandomGenerator) : this(WorldGeometry(worldParameters.worldSize, true, false)) {
this.seed = random.nextLong()
this.worldParameters = worldParameters
this.skyParameters = skyParameters
this.worldLayout = worldParameters.createLayout(random)
}
@ -227,6 +233,50 @@ class WorldTemplate(val geometry: WorldGeometry) {
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) {
var foreground: Registry.Ref<TileDefinition> = BuiltinMetaMaterials.EMPTY.ref
var foregroundMod: Registry.Ref<TileModifierDefinition> = BuiltinMetaMaterials.EMPTY_MOD.ref
@ -455,6 +505,8 @@ class WorldTemplate(val geometry: WorldGeometry) {
}
companion object {
private val LOGGER = LogManager.getLogger()
suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate {
val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!")
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.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.io.DataInputStream
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}")
} else {
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 java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList
// original engine does not have "networked list", so it is always networked
// the dumb way on legacy protocol
@ -42,6 +43,11 @@ class NetworkedList<E>(
private var isInterpolating = false
private var currentTime = 0.0
private var isRemote = false
private val listeners = CopyOnWriteArrayList<Runnable>()
fun addListener(listener: Runnable) {
listeners.add(listener)
}
private fun purgeBacklog() {
while (backlog.size >= maxBacklogSize) {
@ -89,6 +95,7 @@ class NetworkedList<E>(
}
purgeBacklog()
listeners.forEach { it.run() }
}
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
@ -139,6 +146,7 @@ class NetworkedList<E>(
}
purgeBacklog()
listeners.forEach { it.run() }
} else {
readInitial(data, false)
}
@ -235,6 +243,7 @@ class NetworkedList<E>(
elements.add(index, element)
backlog.add(currentVersion() to Entry(index, element))
purgeBacklog()
listeners.forEach { it.run() }
}
override fun addAll(index: Int, elements: Collection<E>): Boolean {
@ -252,6 +261,7 @@ class NetworkedList<E>(
backlog.clear()
backlog.add(currentVersion() to clearEntry)
elements.clear()
listeners.forEach { it.run() }
}
override fun listIterator(): MutableListIterator<E> {
@ -285,6 +295,7 @@ class NetworkedList<E>(
val element = elements.removeAt(index)
backlog.add(currentVersion() to Entry(index))
purgeBacklog()
listeners.forEach { it.run() }
return element
}
@ -308,6 +319,7 @@ class NetworkedList<E>(
val old = elements.set(index, element)
backlog.add(currentVersion() to Entry(index, element))
purgeBacklog()
listeners.forEach { it.run() }
return old
}

View File

@ -52,8 +52,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
private val worlds = HashMap<WorldID, CompletableFuture<ServerWorld>>()
val universe = ServerUniverse()
val chat = ChatHandler(this)
val scope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
val eventLoopScope = CoroutineScope(asCoroutineDispatcher() + SupervisorJob())
val globalScope = CoroutineScope(Starbound.COROUTINE_EXECUTOR + SupervisorJob())
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> {
return supplyAsync {
systemWorlds.computeIfAbsent(location) {
scope.async { loadSystemWorld0(location) }.asCompletableFuture()
globalScope.async { loadSystemWorld0(location) }.asCompletableFuture()
}
}.thenCompose { it }
}
@ -89,6 +88,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
private suspend fun loadInstanceWorld(location: WorldID.Instance): ServerWorld {
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 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 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
}
@ -138,7 +146,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
if (world != null) {
world
} else {
val future = scope.async { loadWorld0(location) }.asCompletableFuture()
val future = globalScope.async { loadWorld0(location) }.asCompletableFuture()
worlds[location] = future
future
}
@ -239,7 +247,7 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
return@removeIf false
}
eventLoopScope.launch {
scope.launch {
try {
it.get().tick()
} catch (err: Throwable) {
@ -289,6 +297,11 @@ sealed class StarboundServer(val root: File) : BlockableEventLoop("Server thread
worlds.values.forEach {
if (it.isDone && !it.isCompletedExceptionally) {
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)

View File

@ -14,9 +14,7 @@ import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
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.FIRST_RESERVED_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.world.Biome
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.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.util.random.random
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_FF
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.AbstractCell
@ -62,27 +61,13 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
/**
* 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
override var state: ChunkState = ChunkState.FRESH
private set
private var isBusy = false
private var idleTicks = 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 temporary = ObjectAVLTreeSet<TimedTicket>()
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
// because ChunkMap is not thread-safe
private val ticketsLock = ReentrantLock()
private val loadJob = world.scope.launch { loadChunk() }
private val loadJob = world.eventLoop.scope.launch { loadChunk() }
var isUnloaded = false
private set
private suspend fun chunkGeneratorLoop() {
while (true) {
if (state == State.FULL)
if (state == ChunkState.FULL)
break
val targetState = targetState.receive()
@ -105,10 +90,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
while (state < targetState) {
isBusy = true
val nextState = State.entries[state.ordinal + 1]
val nextState = ChunkState.entries[state.ordinal + 1]
try {
if (nextState >= State.MICRO_DUNGEONS) {
if (nextState >= ChunkState.MICRO_DUNGEONS) {
val neighbours = ArrayList<ITicket>()
try {
@ -159,8 +144,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
}
when (nextState) {
State.TERRAIN -> {
if (world.template.worldLayout == null) {
ChunkState.TERRAIN -> {
if (world.template.worldLayout == null || world.template.worldParameters is FloatingDungeonWorldParameters) {
// skip since no cells will be generated anyway
cells.value.fill(AbstractCell.EMPTY)
} 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
if (world.template.worldLayout != null) {
if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
placeMicroDungeons()
}
}
State.CAVE_LIQUID -> {
ChunkState.CAVE_LIQUID -> {
// skip if we have no layout
if (world.template.worldLayout != null) {
if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
generateLiquid()
}
}
State.FULL -> {
ChunkState.FULL -> {
CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await()
// skip if we have no layout
if (world.template.worldLayout != null) {
if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
placeGrass()
}
}
State.FRESH -> throw RuntimeException()
ChunkState.FRESH -> throw RuntimeException()
ChunkState.EMPTY -> {} // do nothing
}
bumpState(nextState)
@ -217,7 +203,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
loadCells(cells.value)
// bumping state while loading chunk might have
// 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)
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
return
} else {
bumpState(ChunkState.EMPTY)
// generate.
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 {
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" }
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 {
fun cancel()
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) }
}
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 pos: ChunkPos
get() = this@ServerChunk.pos
@ -336,7 +328,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
final override var listener: IChunkListener? = null
}
private inner class Ticket(state: State) : AbstractTicket(state) {
private inner class Ticket(state: ChunkState) : AbstractTicket(state) {
init {
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
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
require(newState >= state) { "Tried to downgrade $this state from $state to $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) {
idleTicks++
// don't load-save-load-save too frequently
// also make partially-generated chunks stay in memory for way longer, because re-generating
// them is very costly operation
shouldUnload = if (state == State.FULL) idleTicks > 600 else idleTicks > 32000
shouldUnload = if (state == ChunkState.FULL) idleTicks > 600 else idleTicks > 32000
} else {
idleTicks = 0
}
if (shouldUnload) {
unload()
// unload()
return
}
if (state != State.FULL)
if (state != ChunkState.FULL)
return
super.tick()
@ -566,18 +558,18 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
loadJob.cancel()
targetState.close()
if (state == State.FULL) {
if (state == ChunkState.FULL) {
val unloadable = world.entityIndex
.query(
aabb,
filter = Predicate { it.isApplicableForUnloading && aabbd.isInside(it.position) },
filter = Predicate { it.isApplicableForUnloading && !it.isRemote && aabbd.isInside(it.position) },
distinct = true, withEdges = false)
world.storage.saveCells(pos, copyCells())
world.storage.saveEntities(pos, unloadable)
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?
// that's only 6 meta blocks in total!
when (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 -> {}
}
val indexOf = BuiltinMetaMaterials.BIOME_META_MATERIALS.indexOf(tile.material)
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) {
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() {
val placements = CompletableFuture.supplyAsync(Supplier {
val placements = ArrayList<BiomePlaceables.Placement>()
@ -755,12 +749,11 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val random = random(seed)
val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size))
val def = Registries.dungeons[dungeon]
if (def == null) {
LOGGER.error("Unknown dungeon type $dungeon!")
if (dungeon.isEmpty) {
if (missingDungeonNames.add(dungeon.key.left()))
LOGGER.error("Tried to place dungeon ${dungeon.key.left()}, but there is no such dungeon.")
} else {
val anchors = def.value.validAnchors(world)
val anchors = dungeon.value!!.validAnchors(world)
if (anchors.isEmpty())
continue
@ -782,8 +775,12 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
}.orElse(false)
if (!collision && anchor.canPlace(pos.x, pos.y, world)) {
def.value.build(anchor, world, random, pos.x, pos.y, dungeonID = MICRO_DUNGEON_ID).await()
LOGGER.info("Placed dungeon $dungeon at $pos")
try {
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
}
}
@ -820,8 +817,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// since they might be in process of generation, too
// (only if they are in process of generating someing significant, which modify terrain)
// shouldn't be an issue though
val cellAbove = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y + 1)
val cellBelow = world.chunkMap.getCell(pos.tileX + x, pos.tileY + y - 1)
val cellAbove = world.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 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) {
val cell = world.chunkMap.getCell(position).mutable()
val cell = world.getCell(position).mutable()
cell.liquid.state = fillLiquid.entry!!
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
}
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 it.unimi.dsi.fastutil.ints.IntArraySet
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.await
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.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.WarpAction
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.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry
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.RejectedExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier
import kotlin.concurrent.withLock
class ServerWorld private constructor(
val server: StarboundServer,
@ -99,7 +91,7 @@ class ServerWorld private constructor(
clients.add(ServerWorldTracker(this, client, start))
//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 {
isBusy--
}
@ -150,8 +142,6 @@ class ServerWorld private constructor(
eventLoop.scheduleAtFixedRate(::tick, 0L, Starbound.TIMESTEP_NANOS, TimeUnit.NANOSECONDS)
}
val scope = CoroutineScope(eventLoop.asCoroutineDispatcher() + SupervisorJob())
override fun toString(): String {
return "Server World $worldID"
}
@ -159,7 +149,7 @@ class ServerWorld private constructor(
private var idleTicks = 0
private var isBusy = 0
override val isRemote: Boolean
override val isClient: Boolean
get() = false
fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null): TileDamageResult {
@ -273,6 +263,53 @@ class ServerWorld private constructor(
try {
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) {
playerSpawnPosition = findPlayerStart()
}
@ -285,7 +322,7 @@ class ServerWorld private constructor(
// everything inside our own thread, not anywhere else
// This way, external callers can properly wait for preparations to complete
fun prepare(): CompletableFuture<*> {
return scope.launch { prepare0() }.asCompletableFuture()
return eventLoop.scope.launch { prepare0() }.asCompletableFuture()
}
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {
@ -332,7 +369,7 @@ class ServerWorld private constructor(
// dungeon.
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
}
@ -380,29 +417,29 @@ class ServerWorld private constructor(
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)
}
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()
}
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()
}
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)
}
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" }
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" }
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,
// allowing ServerConnection client to track ServerWorld state
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
// this is required because of dumb shit regarding flash time
// 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 {
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)
@ -118,11 +116,11 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
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)
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) {
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
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 java.util.PriorityQueue
import java.util.concurrent.Callable
@ -12,7 +17,6 @@ import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier
@ -29,14 +33,14 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
return executeAt - System.nanoTime()
}
fun shouldEnqueue(): Boolean {
if (executeAt <= System.nanoTime())
return perform()
fun shouldEnqueue(isShutdown: Boolean): Boolean {
if (isShutdown || executeAt <= System.nanoTime())
return perform(isShutdown)
return true
}
fun perform(): Boolean {
fun perform(isShutdown: Boolean): Boolean {
if (repeat) {
if (isFixedDelay) {
// fixed delay
@ -67,7 +71,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
executeAt = now + timeDelay + deadlineMargin - (now - timeBefore)
}
return true
return !isShutdown
} else {
run()
return false
@ -79,6 +83,8 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
private val eventQueue = LinkedBlockingQueue<TaskPair<*>>()
private val scheduledQueue = PriorityQueue<ScheduledTask<*>>()
val coroutines = asCoroutineDispatcher()
val scope = CoroutineScope(coroutines + SupervisorJob())
private fun nextDeadline(): Long {
if (isShutdown)
@ -95,7 +101,6 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
@Volatile
private var isShutdown = false
private var isRunning = true
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<*>>()
var lastSize: Int
do {
lastSize = executed.size
while (scheduledQueue.isNotEmpty() && scheduledQueue.peek()!!.executeAt <= System.nanoTime() && !isShutdown) {
while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.executeAt <= System.nanoTime())) {
executedAnything = true
val poll = scheduledQueue.poll()!!
if (poll.perform()) {
if (poll.perform(isShutdown)) {
executed.add(poll)
}
}
} while (lastSize != executed.size && !isShutdown)
} while (lastSize != executed.size)
scheduledQueue.addAll(executed)
}
@ -152,6 +157,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
if (isShutdown && isRunning) {
while (eventLoopIteration()) {}
isRunning = false
scope.cancel(CancellationException("EventLoop shut down"))
performShutdown()
}
}
@ -160,7 +166,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
}
final override fun execute(command: Runnable) {
if (isShutdown)
if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down")
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> {
if (isShutdown)
if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down")
if (currentThread() === this) {
@ -216,7 +222,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
fun isSameThread() = this === currentThread()
final override fun submit(task: Runnable): CompletableFuture<*> {
if (isShutdown)
if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down")
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> {
if (isShutdown)
if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down")
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>> {
if (isShutdown)
if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down")
return tasks.map { submit(it) }
}
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")
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 {
if (isShutdown)
throw RejectedExecutionException("EventLoop is shutting down")
if (!isRunning)
throw RejectedExecutionException("EventLoop shut down")
return submit(tasks.first()).get()
}
final override fun <T> invokeAny(tasks: Collection<Callable<T>>, timeout: Long, unit: TimeUnit): T {
if (isShutdown)
if (!isRunning)
throw RejectedExecutionException("EventLoop is shutting down")
return submit(tasks.first()).get(timeout, unit)
@ -311,6 +317,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
}
isRunning = false
scope.cancel(CancellationException("EventLoop shut down"))
performShutdown()
} else {
// 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> {
if (!isShutdown) {
isShutdown = true
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 {
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)
execute {
if (task.shouldEnqueue())
if (task.shouldEnqueue(isShutdown))
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)
execute {
if (task.shouldEnqueue())
if (task.shouldEnqueue(isShutdown))
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)
execute {
if (task.shouldEnqueue())
if (task.shouldEnqueue(isShutdown))
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)
execute {
if (task.shouldEnqueue())
if (task.shouldEnqueue(isShutdown))
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.Vector2i
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.ICellAccess
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
@ -42,6 +43,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0
private set
abstract val state: ChunkState
val width = (world.geometry.size.x - pos.tileX).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]
}
override fun getCellDirect(x: Int, y: Int): AbstractCell {
return getCell(x, y)
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
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.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray
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.packets.clientbound.SetPlayerStartPacket
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.random.random
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.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.io.Closeable
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.random.RandomGenerator
import java.util.stream.Stream
import kotlin.concurrent.withLock
import kotlin.math.roundToInt
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()
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 {
return chunkMap.getCell(x, y)
}
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) { }
@ -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)
}
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 iy = geometry.y.cell(y)
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())
@ -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()
/**
* Random generator for in-world events
*/
val random: RandomGenerator = random()
var gravity = Vector2d(0.0, -80.0)
abstract val isRemote: Boolean
abstract val isClient: Boolean
val isServer: Boolean
get() = !isClient
// generic lock
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.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.io.DataInputStream
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)
}
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
fun write(stream: DataOutputStream) {

View File

@ -1,7 +1,7 @@
package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkState
interface ICellAccess {
/**
@ -10,16 +10,13 @@ interface ICellAccess {
fun getCell(x: Int, y: Int): AbstractCell
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
*/
fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean
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
fun getTile(x: Int, y: Int): AbstractTileState
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 {
return getTile(x, y).material.value.collisionKind

View File

@ -22,6 +22,22 @@ data class MutableTileState(
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 {
if (value < 0) {
hueShift = 0f

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kommons.util.IStruct2i
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 {
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)
}
override fun getCellDirect(x: Int, y: Int): AbstractCell {
return parent.getCellDirect(x + this.x, y + this.y)
override fun setCell(x: Int, y: Int, cell: AbstractCell, chunkState: ChunkState): Boolean {
return parent.setCell(x + this.x, y + this.y, cell, chunkState)
}
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 {
return getCell(x, y).foreground
}
override fun getTileDirect(x: Int, y: Int): AbstractTileState {
return getCellDirect(x, y).foreground
}
}
class Background(parent: ICellAccess) : TileView(parent) {
override fun getTile(x: Int, y: Int): AbstractTileState {
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.JsonElement
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.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.InteractRequest
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.MasterElement
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 serverWorld get() = world as ServerWorld
val isSpawned: Boolean
val isInWorld: Boolean
get() = innerWorld != null
abstract val type: EntityType
@ -88,8 +86,19 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
open val isApplicableForUnloading: Boolean
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 onRemove(world: World<*, *>, isDeath: Boolean) { }
protected open fun onRemove(world: World<*, *>, reason: RemovalReason) { }
val networkGroup = MasterElement(NetworkedGroup())
abstract fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean)
@ -125,20 +134,26 @@ abstract class AbstractEntity(path: String) : JsonDriven(path), Comparable<Abstr
onJoinWorld(world)
}
fun remove(isDeath: Boolean = false) {
fun remove(reason: RemovalReason) {
val world = innerWorld ?: throw IllegalStateException("Not in world")
world.eventLoop.ensureSameThread()
mailbox.shutdownNow()
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 = null
innerWorld = null
if (world is ServerWorld) {
world.clients.forEach {
it.forget(this, isDeath)
it.forget(this, reason)
}
}
}

View File

@ -207,7 +207,7 @@ class ActorMovementController() : MovementController() {
override fun move() {
// TODO: anchor entity
if (anchorEntity?.isSpawned != true)
if (anchorEntity?.isInWorld != true)
anchorEntity = null
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.NetworkedGroup
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.NetworkedSignal
import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable
@ -125,7 +126,7 @@ class Animator() {
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 yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear }
var volumeTarget by networkedFloat(1.0).also { elements.add(it) }
@ -474,10 +475,10 @@ class Animator() {
val sound = Sound()
if (v.isLeft) {
sound.soundPool = v.left()
sound.soundPool.addAll(v.left())
} else {
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.yPosition = conf.position.y
sound.volumeTarget = conf.volume

View File

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

View File

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

View File

@ -1,10 +1,21 @@
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.vector.Vector2d
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.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
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.entities.AbstractEntity
@ -16,21 +27,16 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
protected val yTilePositionNet = networkedSignedInt()
init {
xTilePositionNet.addListener(::updateSpatialIndex)
yTilePositionNet.addListener(::updateSpatialIndex)
xTilePositionNet.addListener(::onPositionUpdated)
yTilePositionNet.addListener(::onPositionUpdated)
}
abstract val metaBoundingBox: AABB
protected open fun updateSpatialIndex() {
val spatialEntry = spatialEntry ?: return
spatialEntry.fixture.move(metaBoundingBox + position)
}
var xTilePosition: Int
get() = xTilePositionNet.get()
set(value) {
if (isSpawned) {
if (isInWorld) {
xTilePositionNet.accept(world.geometry.x.cell(value))
} else {
xTilePositionNet.accept(value)
@ -40,7 +46,7 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
var yTilePosition: Int
get() = yTilePositionNet.get()
set(value) {
if (isSpawned) {
if (isInWorld) {
yTilePositionNet.accept(world.geometry.x.cell(value))
} else {
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)
*/
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)
*/
abstract val roots: Set<Vector2i>
abstract val roots: Collection<Vector2i>
abstract fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean
override fun onJoinWorld(world: World<*, *>) {
updateSpatialIndex()
private var needToUpdateSpaces = false
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.InteractAction
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.quest.QuestArcDescriptor
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))
for ((k, v) in data.get("parameters") { JsonObject() }.entrySet()) {
parameters[k] = v
loadParameters(data.get("parameters") { JsonObject() })
}
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 {
networkGroup.upstream.add(it)
it.addListener(Runnable {
invalidate()
})
it.addListener { invalidate() }
}
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 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 {
networkGroup.upstream.add(xTilePositionNet)
networkGroup.upstream.add(yTilePositionNet)
networkedMaterialSpaces.addListener {
materialSpaces0.invalidate()
markSpacesDirty()
}
}
var direction by networkedEnum(Direction.LEFT).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)
it.addListener(Runnable { invalidate() })
}
@ -194,10 +225,16 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
init {
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 {
animator = Animator().also { networkGroup.upstream.add(it.networkGroup) }
animator = Animator()
}
networkGroup.upstream.add(animator.networkGroup)
}
val unbreakable by LazyData {
@ -246,35 +283,79 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
}
init {
networkedRenderKeys.addListener(Runnable { drawablesCache.invalidate() })
networkedRenderKeys.addListener { drawablesCache.invalidate() }
}
val drawables: List<Drawable> by drawablesCache
private val occupySpaces0 = LazyData {
(orientation?.occupySpaces ?: setOf()).stream().map { world.geometry.wrap(it + tilePosition) }.collect(ImmutableSet.toImmutableSet())
private fun updateOrientation() {
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>
get() = setOf()
orientationIndex = index.toLong()
private val anchorPositions0 = LazyData {
immutableSet {
orientation?.anchors?.forEach { accept(it.pos + tilePosition) }
val orientation = orientation
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()
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"
}
@ -300,7 +381,6 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
override fun interact(request: InteractRequest): InteractAction {
val diff = world.geometry.diff(request.sourcePos, position)
// val result =
if (!interactAction.isJsonNull) {
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 {
if (unbreakable)
return false
@ -415,5 +490,21 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
result.deserialize(content)
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
}
}
}