Merge TicketList into ServerChunk

This commit is contained in:
DBotThePony 2024-04-05 19:55:45 +07:00
parent c3c928de92
commit a839af1041
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 373 additions and 489 deletions

View File

@ -1,9 +1,16 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
@ -16,11 +23,16 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePac
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.util.concurrent.CompletableFuture
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import kotlin.concurrent.withLock
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
/** /**
@ -43,7 +55,236 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
var state: State = State.FRESH var state: State = State.FRESH
private set private set
fun bumpState(newState: State) { private var isBusy = false
private var idleTicks = 0
private var ticks = 0
private val targetState = Channel<State>(Int.MAX_VALUE)
private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var nextTicketID = 0
// ticket lock because tickets *could* be canceled (or created) concurrently
// 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() }
var isUnloaded = false
private set
private suspend fun chunkGeneratorLoop() {
while (true) {
if (state == State.FULL)
break
val targetState = targetState.receive()
while (state < targetState) {
isBusy = true
val nextState = ServerChunk.State.entries[state.ordinal + 1]
try {
when (nextState) {
State.TILES -> {
// tiles can be generated concurrently without any consequences
CompletableFuture.runAsync(Runnable { prepareCells() }, Starbound.EXECUTOR).await()
}
State.MICRO_DUNGEONS -> {
//LOGGER.error("NYI: Generating microdungeons for $chunk")
}
State.CAVE_LIQUID -> {
//LOGGER.error("NYI: Generating cave liquids for $chunk")
}
State.TILE_ENTITIES -> {
//LOGGER.error("NYI: Generating tile entities for $chunk")
}
State.ENTITIES -> {
//LOGGER.error("NYI: Placing entities for $chunk")
}
else -> {}
}
bumpState(nextState)
} catch (err: Throwable) {
LOGGER.error("Exception while propagating $this to next generation state $nextState", err)
break
}
}
isBusy = false
}
isBusy = false
}
private suspend fun loadChunk() {
try {
val cells = world.storage.loadCells(pos).await()
// very good.
if (cells.isPresent) {
loadCells(cells.value)
bumpState(State.CAVE_LIQUID)
world.storage.loadEntities(pos).await().ifPresent {
for (obj in it) {
obj.joinWorld(world)
}
}
bumpState(State.FULL)
isBusy = false
return
} else {
// generate.
chunkGeneratorLoop()
}
} catch (err: Throwable) {
LOGGER.error("Exception while loading chunk $this", err)
}
}
fun permanentTicket(target: State = State.FULL): ITicket {
ticketsLock.withLock {
return Ticket(target)
}
}
fun temporaryTicket(time: Int, target: State = State.FULL): ITimedTicket {
require(time > 0) { "Invalid ticket time: $time" }
ticketsLock.withLock {
return TimedTicket(time, target)
}
}
interface ITicket {
fun cancel()
val isCanceled: Boolean
val pos: ChunkPos
val id: Int
val chunk: CompletableFuture<ServerChunk>
var listener: IChunkListener?
}
interface ITimedTicket : ITicket, Comparable<ITimedTicket> {
val timeRemaining: Int
fun prolong(ticks: Int)
override fun compareTo(other: ITimedTicket): Int {
val cmp = timeRemaining.compareTo(other.timeRemaining)
if (cmp != 0) return cmp
return id.compareTo(other.id)
}
}
override fun cellChanges(x: Int, y: Int, cell: ImmutableCell) {
super.cellChanges(x, y, cell)
val permanent: List<Ticket>
val temporary: List<TimedTicket>
ticketsLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.temporary)
}
permanent.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
temporary.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
}
private fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
val permanent: List<Ticket>
val temporary: List<TimedTicket>
ticketsLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.temporary)
}
permanent.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
temporary.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
}
private abstract inner class AbstractTicket(val targetState: State) : ITicket {
final override val id: Int = nextTicketID++
final override val pos: ChunkPos
get() = this@ServerChunk.pos
final override var isCanceled: Boolean = false
final override val chunk = CompletableFuture<ServerChunk>()
init {
isBusy = true
if (this@ServerChunk.state >= targetState) {
chunk.complete(this@ServerChunk)
} else {
this@ServerChunk.targetState.trySend(targetState)
}
}
final override fun cancel() {
if (isCanceled) return
ticketsLock.withLock {
if (isCanceled) return
isCanceled = true
chunk.cancel(false)
listener = null
cancel0()
}
}
protected abstract fun cancel0()
final override var listener: IChunkListener? = null
}
private inner class Ticket(state: State) : AbstractTicket(state) {
init {
permanent.add(this)
}
override fun cancel0() {
permanent.remove(this)
}
}
private inner class TimedTicket(expiresAt: Int, state: ServerChunk.State) : AbstractTicket(state), ITimedTicket {
var expiresAt = expiresAt + ticks
override val timeRemaining: Int
get() = (expiresAt - ticks).coerceAtLeast(0)
init {
temporary.add(this)
}
override fun cancel0() {
temporary.remove(this)
}
override fun prolong(ticks: Int) {
if (ticks == 0 || isCanceled) return
ticketsLock.withLock {
if (isCanceled) return
temporary.remove(this)
expiresAt += ticks
if (timeRemaining > 0) temporary.add(this)
}
}
}
private fun bumpState(newState: State) {
if (newState == state) return
require(newState >= state) { "Tried to downgrade $this state from $state to $newState" } require(newState >= state) { "Tried to downgrade $this state from $state to $newState" }
if (newState >= State.ENTITIES) { if (newState >= State.ENTITIES) {
@ -51,6 +292,17 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} else { } else {
this.state = newState this.state = newState
} }
val permanent: List<Ticket>
val temporary: List<TimedTicket>
ticketsLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.temporary)
}
permanent.forEach { if (it.targetState <= state) it.chunk.complete(this) }
temporary.forEach { if (it.targetState <= state) it.chunk.complete(this) }
} }
fun copyCells(): Object2DArray<ImmutableCell> { fun copyCells(): Object2DArray<ImmutableCell> {
@ -85,14 +337,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y] val health = (if (isBackground) tileHealthBackground else tileHealthForeground).value[pos.x, pos.y]
val tile = cell.tile(isBackground) val tile = cell.tile(isBackground)
val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) { val params = if (!damage.type.isPenetrating && tile.modifier.value.breaksWithTile) {
tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable tile.material.value.actualDamageTable + tile.modifier.value.actualDamageTable
} else { } else {
tile.material.value.actualDamageTable tile.material.value.actualDamageTable
} }
health.damage(params, sourcePosition, damage) health.damage(params, sourcePosition, damage)
subscribers.forEach { it.onTileHealthUpdate(pos.x, pos.y, isBackground, health) } onTileHealthUpdate(pos.x, pos.y, isBackground, health)
if (health.isDead) { if (health.isDead) {
if (isBackground) { if (isBackground) {
@ -165,6 +417,31 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
override fun tick() { override fun tick() {
ticks++
ticketsLock.withLock {
while (temporary.isNotEmpty() && temporary.first().timeRemaining <= 0) {
val ticket = temporary.first()
ticket.isCanceled = true
temporary.remove(ticket)
}
}
var shouldUnload = !isBusy && temporary.isEmpty() && permanent.isEmpty()
if (shouldUnload) {
idleTicks++
// don't load-save-load-save too frequently
shouldUnload = idleTicks > 600
} else {
idleTicks = 0
}
if (shouldUnload) {
unload()
return
}
if (state != State.FULL) if (state != State.FULL)
return return
@ -178,23 +455,50 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
damagedTilesBackground.removeIf { (x, y) -> damagedTilesBackground.removeIf { (x, y) ->
val health = tileHealthBackground[x, y] val health = tileHealthBackground[x, y]
val result = !health.tick(cells[x, y].background.material.value.actualDamageTable) val result = !health.tick(cells[x, y].background.material.value.actualDamageTable)
subscribers.forEach { it.onTileHealthUpdate(x, y, true, health) } onTileHealthUpdate(x, y, true, health)
result result
} }
damagedTilesForeground.removeIf { (x, y) -> damagedTilesForeground.removeIf { (x, y) ->
val health = tileHealthForeground[x, y] val health = tileHealthForeground[x, y]
val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable) val result = !health.tick(cells[x, y].foreground.material.value.actualDamageTable)
subscribers.forEach { it.onTileHealthUpdate(x, y, false, health) } onTileHealthUpdate(x, y, false, health)
result result
} }
} }
} }
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> { fun unload() {
val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE) if (isUnloaded)
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE) return
isUnloaded = true
loadJob.cancel()
targetState.close()
if (state == State.FULL) {
val unloadable = world.entityIndex
.query(
aabb,
filter = Predicate { it.isApplicableForUnloading && aabb.isInside(it.position) },
distinct = true, withEdges = false)
world.storage.saveCells(pos, copyCells())
world.storage.saveEntities(pos, unloadable)
unloadable.forEach {
it.remove()
}
}
world.chunkMap.remove(pos)
}
fun cancelLoadJob() {
loadJob.cancel()
}
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
if (cells.isInitialized()) { if (cells.isInitialized()) {
val cells = cells.value val cells = cells.value
return Object2DArray(width, height) { a, b -> cells[a, b].toLegacyNet() } return Object2DArray(width, height) { a, b -> cells[a, b].toLegacyNet() }
@ -206,9 +510,6 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
fun prepareCells() { fun prepareCells() {
val cells = cells.value val cells = cells.value
val width = (world.geometry.size.x - pos.tileX).coerceAtMost(cells.columns)
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(cells.rows)
for (x in 0 until width) { for (x in 0 until width) {
for (y in 0 until height) { for (y in 0 until height) {
val info = world.template.cellInfo(pos.tileX + x, pos.tileY + y) val info = world.template.cellInfo(pos.tileX + x, pos.tileY + y)
@ -261,4 +562,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
} }
companion object {
private val LOGGER = LogManager.getLogger()
}
} }

View File

@ -3,16 +3,10 @@ package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
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 it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,11 +37,8 @@ import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
@ -57,11 +48,8 @@ import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.function.Supplier import java.util.function.Supplier
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.properties.Delegates
class ServerWorld private constructor( class ServerWorld private constructor(
val server: StarboundServer, val server: StarboundServer,
@ -169,8 +157,8 @@ class ServerWorld private constructor(
super.close() super.close()
spinner.unpause() spinner.unpause()
ticketListLock.withLock { chunkMap.chunks().forEach {
ticketLists.forEach { it.scope.cancel() } it.cancelLoadJob()
} }
clients.forEach { clients.forEach {
@ -289,6 +277,7 @@ class ServerWorld private constructor(
override fun tick() { override fun tick() {
super.tick() super.tick()
val packet = StepUpdatePacket(ticks) val packet = StepUpdatePacket(ticks)
clients.forEach { clients.forEach {
@ -303,10 +292,6 @@ class ServerWorld private constructor(
} }
} }
} }
ticketListLock.withLock {
ticketLists.removeIf { it.tick() }
}
} }
override fun broadcast(packet: IPacket) { override fun broadcast(packet: IPacket) {
@ -337,7 +322,7 @@ class ServerWorld private constructor(
} }
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d { private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {
val tickets = ArrayList<ITicket>() val tickets = ArrayList<ServerChunk.ITicket>()
try { try {
LOGGER.info("Trying to find player spawn position...") LOGGER.info("Trying to find player spawn position...")
@ -424,387 +409,32 @@ class ServerWorld private constructor(
return ServerChunk(this, pos) return ServerChunk(this, pos)
} }
private val ticketMap = Long2ObjectOpenHashMap<TicketList>() fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITicket? {
private val ticketLists = ArrayList<TicketList>() return chunkMap.compute(pos)?.permanentTicket(target)
private val ticketListLock = ReentrantLock()
private fun getTicketList(pos: ChunkPos): TicketList {
return ticketMap.computeIfAbsent(geometry.wrapToLong(pos), Long2ObjectFunction { TicketList(it) })
} }
fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ITicket { fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITicket> {
ticketListLock.withLock { return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
return getTicketList(pos).Ticket(target)
}
} }
fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List<ITicket> { fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITicket> {
ticketListLock.withLock { return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) }
}
} }
fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List<ITicket> { fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITimedTicket? {
ticketListLock.withLock { return chunkMap.compute(pos)?.temporaryTicket(time, target)
return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) }
}
} }
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): ITimedTicket { fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" } require(time > 0) { "Invalid ticket time: $time" }
ticketListLock.withLock { return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
return getTicketList(pos).TimedTicket(time, target)
}
} }
fun temporaryChunkTicket(region: AABBi, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ITimedTicket> { fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" } require(time > 0) { "Invalid ticket time: $time" }
ticketListLock.withLock { return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
return geometry.region2Chunks(region).map { getTicketList(it).TimedTicket(time, target) }
}
}
fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List<ITimedTicket> {
require(time > 0) { "Invalid ticket time: $time" }
ticketListLock.withLock {
return geometry.region2Chunks(region).map { getTicketList(it).TimedTicket(time, target) }
}
}
override fun onChunkCreated(chunk: ServerChunk) {
ticketListLock.withLock {
ticketMap[chunk.pos.toLong()]?.let { chunk.addListener(it) }
}
}
override fun onChunkRemoved(chunk: ServerChunk) {
ticketListLock.withLock {
ticketMap[chunk.pos.toLong()]?.let { chunk.removeListener(it) }
}
}
interface ITicket {
fun cancel()
val isCanceled: Boolean
val pos: ChunkPos
val id: Int
val chunk: CompletableFuture<ServerChunk>
var listener: IChunkListener?
}
interface ITimedTicket : ITicket, Comparable<ITimedTicket> {
val timeRemaining: Int
fun prolong(ticks: Int)
override fun compareTo(other: ITimedTicket): Int {
val cmp = timeRemaining.compareTo(other.timeRemaining)
if (cmp != 0) return cmp
return id.compareTo(other.id)
}
}
private inner class TicketList(val pos: ChunkPos) : IChunkListener {
constructor(pos: Long) : this(ChunkPos(pos))
private var calledLoadChunk = true
private val permanent = ArrayList<Ticket>()
private val temporary = ObjectAVLTreeSet<TimedTicket>()
private var ticks = 0
private var nextTicketID = 0
private var isBusy = false
private var chunk by Delegates.notNull<ServerChunk>()
private val targetState = Channel<ServerChunk.State>(Int.MAX_VALUE)
val scope = CoroutineScope(mailbox.asCoroutineDispatcher())
private var idleTicks = 0
private var isRemoved = false
private suspend fun chunkGeneratorLoop() {
while (true) {
if (chunk.state == ServerChunk.State.FULL) {
break
}
val targetState = targetState.receive()
while (chunk.state < targetState) {
isBusy = true
val nextState = ServerChunk.State.entries[chunk.state.ordinal + 1]
try {
when (nextState) {
ServerChunk.State.TILES -> {
// tiles can be generated concurrently without any consequences
CompletableFuture.runAsync(Runnable { chunk.prepareCells() }, Starbound.EXECUTOR).await()
}
ServerChunk.State.MICRO_DUNGEONS -> {
//LOGGER.error("NYI: Generating microdungeons for $chunk")
}
ServerChunk.State.CAVE_LIQUID -> {
//LOGGER.error("NYI: Generating cave liquids for $chunk")
}
ServerChunk.State.TILE_ENTITIES -> {
//LOGGER.error("NYI: Generating tile entities for $chunk")
}
ServerChunk.State.ENTITIES -> {
//LOGGER.error("NYI: Placing entities for $chunk")
}
else -> {}
}
chunk.bumpState(nextState)
fulfilFutures()
} catch (err: Throwable) {
LOGGER.error("Exception while propagating $chunk to next generation state $nextState", err)
break
}
}
isBusy = false
}
isBusy = false
}
private suspend fun loadChunk0() {
try {
val cells = storage.loadCells(pos).await()
// very good.
if (cells.isPresent) {
chunk.loadCells(cells.value)
chunk.bumpState(ServerChunk.State.CAVE_LIQUID)
fulfilFutures()
storage.loadEntities(pos).await().ifPresent {
for (obj in it) {
obj.joinWorld(this@ServerWorld)
}
}
chunk.bumpState(ServerChunk.State.FULL)
fulfilFutures()
isBusy = false
return
} else {
// generate.
chunkGeneratorLoop()
}
} catch (err: Throwable) {
LOGGER.error("Exception while loading chunk $chunk", err)
}
}
private fun loadChunk() {
if (!calledLoadChunk)
return
calledLoadChunk = true
if (geometry.x.inBoundsChunk(pos.x) && geometry.y.inBoundsChunk(pos.y)) {
ticketListLock.withLock {
ticketLists.add(this)
}
val existing = chunkMap[pos]
if (existing == null) {
// fresh chunk
val chunk = chunkMap.compute(pos) ?: return ticketListLock.withLock {
isRemoved = true
ticketLists.remove(this)
ticketMap.remove(pos.toLong())
}
this.chunk = chunk
chunk.addListener(this)
isBusy = true
scope.launch { loadChunk0() }
fulfilFutures()
} else {
chunk = existing
existing.addListener(this)
fulfilFutures()
}
}
}
private fun unload() {
if (isRemoved)
return
isRemoved = true
scope.cancel()
targetState.close()
val removed = ticketMap.remove(pos.toLong())
check(removed == this) { "Expected to remove $this, but removed $removed" }
if (chunk.state == ServerChunk.State.FULL) {
val unloadable = entityIndex
.query(
chunk.aabb,
filter = Predicate { it.isApplicableForUnloading && chunk.aabb.isInside(it.position) },
distinct = true, withEdges = false)
storage.saveCells(pos, chunk.copyCells())
storage.saveEntities(pos, unloadable)
unloadable.forEach {
it.remove()
}
}
chunkMap.remove(pos)
}
fun tick(): Boolean {
ticks++
while (temporary.isNotEmpty() && temporary.first().timeRemaining <= 0) {
val ticket = temporary.first()
ticket.isCanceled = true
temporary.remove(ticket)
}
var shouldUnload = !isBusy && temporary.isEmpty() && permanent.isEmpty()
if (shouldUnload) {
idleTicks++
// don't load-save-load-save too frequently
shouldUnload = idleTicks > 600
} else {
idleTicks = 0
}
if (shouldUnload) {
unload()
}
return shouldUnload
}
private fun fulfilFutures() {
val permanent: List<Ticket>
val temporary: List<Ticket>
ticketListLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.permanent)
}
val state = chunk.state
permanent.forEach { if (it.targetState <= state) it.chunk.complete(chunk) }
temporary.forEach { if (it.targetState <= state) it.chunk.complete(chunk) }
}
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
val permanent: List<Ticket>
val temporary: List<Ticket>
ticketListLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.permanent)
}
val state = chunk.state
permanent.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
temporary.forEach { if (it.targetState <= state) it.listener?.onCellChanges(x, y, cell) }
}
override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
val permanent: List<Ticket>
val temporary: List<Ticket>
ticketListLock.withLock {
permanent = ObjectArrayList(this.permanent)
temporary = ObjectArrayList(this.permanent)
}
val state = chunk.state
permanent.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
temporary.forEach { if (it.targetState <= state) it.listener?.onTileHealthUpdate(x, y, isBackground, health) }
}
abstract inner class AbstractTicket(val targetState: ServerChunk.State) : ITicket {
final override val id: Int = nextTicketID++
final override val pos: ChunkPos
get() = this@TicketList.pos
final override var isCanceled: Boolean = false
final override val chunk = CompletableFuture<ServerChunk>()
init {
isBusy = true
this@TicketList.targetState.trySend(targetState)
}
final override fun cancel() {
if (isCanceled) return
ticketListLock.withLock {
if (isCanceled) return
isCanceled = true
chunk.cancel(false)
listener = null
cancel0()
}
}
protected abstract fun cancel0()
final override var listener: IChunkListener? = null
}
inner class Ticket(state: ServerChunk.State) : AbstractTicket(state) {
init {
permanent.add(this)
loadChunk()
}
override fun cancel0() {
permanent.remove(this)
}
}
inner class TimedTicket(expiresAt: Int, state: ServerChunk.State) : AbstractTicket(state), ITimedTicket {
var expiresAt = expiresAt + ticks
override val timeRemaining: Int
get() = (expiresAt - ticks).coerceAtLeast(0)
init {
temporary.add(this)
loadChunk()
}
override fun cancel0() {
temporary.remove(this)
}
override fun prolong(ticks: Int) {
if (ticks == 0 || isCanceled) return
ticketListLock.withLock {
if (isCanceled) return
temporary.remove(this)
expiresAt += ticks
if (timeRemaining > 0) temporary.add(this)
}
}
}
} }
@JsonFactory @JsonFactory

View File

@ -73,7 +73,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
tasks.add(task) tasks.add(task)
} }
private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener { private inner class Ticket(val ticket: ServerChunk.ITicket, val pos: ChunkPos) : IChunkListener {
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet())) send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
} }
@ -182,7 +182,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
for (pos in newTrackedChunks) { for (pos in newTrackedChunks) {
if (pos !in tickets) { if (pos !in tickets) {
val ticket = world.permanentChunkTicket(pos) val ticket = world.permanentChunkTicket(pos) ?: continue
val thisTicket = Ticket(ticket, pos) val thisTicket = Ticket(ticket, pos)
tickets[pos] = thisTicket tickets[pos] = thisTicket

View File

@ -40,7 +40,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
protected val subscribers = CopyOnWriteArraySet<IChunkListener>() val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE)
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE)
// local cells' tile access // local cells' tile access
val localBackgroundView = TileView.Background(this) val localBackgroundView = TileView.Background(this)
@ -53,6 +54,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble())
// TODO: maybe fit them into "width" and "height" variables added recently?
protected val cells = lazy { protected val cells = lazy {
Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL) Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL)
} }
@ -88,30 +90,25 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
val ix = x and CHUNK_SIZE_FF val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
val iy = y and CHUNK_SIZE_FF
if (ix != x || iy != y) return false
val old = if (cells.isInitialized()) cells.value[ix, iy] else AbstractCell.NULL
val new = cell.immutable() val new = cell.immutable()
if (old != new) { if (old != new) {
cells.value[ix, iy] = new cells.value[x, y] = new
if (old.foreground != new.foreground) { if (old.foreground != new.foreground) {
foregroundChanges(ix, iy, new) foregroundChanges(x, y, new)
} }
if (old.background != new.background) { if (old.background != new.background) {
backgroundChanges(ix, iy, new) backgroundChanges(x, y, new)
} }
if (old.liquid != new.liquid) { if (old.liquid != new.liquid) {
liquidChanges(ix, iy, new) liquidChanges(x, y, new)
} }
cellChanges(ix, iy, new) cellChanges(x, y, new)
} }
return true return true
@ -137,8 +134,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected open fun cellChanges(x: Int, y: Int, cell: ImmutableCell) { protected open fun cellChanges(x: Int, y: Int, cell: ImmutableCell) {
changeset++ changeset++
cellChangeset++ cellChangeset++
subscribers.forEach { it.onCellChanges(x, y, cell) }
} }
protected inline fun forEachNeighbour(block: (This) -> Unit) { protected inline fun forEachNeighbour(block: (This) -> Unit) {
@ -152,14 +147,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
world.chunkMap[pos.bottomRight]?.let(block) world.chunkMap[pos.bottomRight]?.let(block)
} }
fun addListener(subscriber: IChunkListener): Boolean {
return subscribers.add(subscriber)
}
fun removeListener(subscriber: IChunkListener): Boolean {
return subscribers.remove(subscriber)
}
override fun toString(): String { override fun toString(): String {
return "${this::class.simpleName}(pos=$pos, world=$world)" return "${this::class.simpleName}(pos=$pos, world=$world)"
} }

View File

@ -5,6 +5,8 @@ import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
@ -44,6 +46,7 @@ import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.concurrent.withLock
import kotlin.math.roundToInt import kotlin.math.roundToInt
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess, Closeable { abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess, Closeable {
@ -71,21 +74,31 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected open fun onChunkCreated(chunk: ChunkType) { } protected open fun onChunkCreated(chunk: ChunkType) { }
protected open fun onChunkRemoved(chunk: ChunkType) { } protected open fun onChunkRemoved(chunk: ChunkType) { }
abstract inner class ChunkMap : Iterable<ChunkType> { abstract inner class ChunkMap {
abstract operator fun get(x: Int, y: Int): ChunkType? abstract operator fun get(x: Int, y: Int): ChunkType?
abstract fun compute(x: Int, y: Int): ChunkType? abstract fun compute(x: Int, y: Int): ChunkType?
fun compute(pos: ChunkPos) = compute(pos.x, pos.y) abstract fun chunks(): List<ChunkType>
abstract fun remove(x: Int, y: Int) abstract fun remove(x: Int, y: Int)
fun remove(pos: ChunkPos) = remove(pos.x, pos.y)
abstract fun getCell(x: Int, y: Int): AbstractCell private val chunkCache = arrayOfNulls<Chunk<*, *>>(4)
abstract fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean
operator fun get(pos: ChunkPos) = get(pos.x, pos.y) operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
protected fun create(x: Int, y: Int): ChunkType { fun compute(pos: ChunkPos) = compute(pos.x, pos.y)
return chunkFactory(ChunkPos(x, y)) fun remove(pos: ChunkPos) = remove(pos.x, pos.y)
fun getCell(x: Int, y: Int): AbstractCell {
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
val chunk = get(geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)) ?: return AbstractCell.NULL
return chunk.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK)
}
fun setCell(x: Int, y: Int, cell: AbstractCell): 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)
} }
abstract val size: Int abstract val size: Int
@ -95,13 +108,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
inner class SparseChunkMap : ChunkMap() { inner class SparseChunkMap : ChunkMap() {
private val map = Long2ObjectOpenHashMap<ChunkType>() private val map = Long2ObjectOpenHashMap<ChunkType>()
override fun getCell(x: Int, y: Int): AbstractCell {
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return AbstractCell.NULL
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
return this[geometry.x.chunkFromCell(ix), geometry.y.chunkFromCell(iy)]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: AbstractCell.NULL
}
override fun get(x: Int, y: Int): ChunkType? { override fun get(x: Int, y: Int): ChunkType? {
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[ChunkPos.toLong(x, y)] return map[ChunkPos.toLong(x, y)]
@ -109,23 +115,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
override fun compute(x: Int, y: Int): ChunkType? { override fun compute(x: Int, y: Int): ChunkType? {
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
val index = ChunkPos.toLong(x, y) val index = ChunkPos.toLong(x, y)
val get = map[index] ?: create(x, y).also { map[index] = it; onChunkCreated(it) } return map[index] ?: chunkFactory(ChunkPos(x, y)).also { map[index] = it; onChunkCreated(it) }
return get
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return false
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
val cx = geometry.x.chunkFromCell(ix)
val cy = geometry.y.chunkFromCell(iy)
val index = ChunkPos.toLong(cx, cy)
val get = map[index] ?: create(cx, cy).also { map[index] = it }
return get.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
@ -139,8 +130,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
} }
override fun iterator(): Iterator<ChunkType> { override fun chunks(): List<ChunkType> {
return map.values.iterator() return ObjectArrayList(map.values)
} }
override val size: Int override val size: Int
@ -149,29 +140,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
inner class ArrayChunkMap : ChunkMap() { inner class ArrayChunkMap : ChunkMap() {
private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE)) private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE))
private val existing = ObjectArraySet<ChunkPos>() private val existing = ObjectAVLTreeSet<ChunkPos>()
private fun getRaw(x: Int, y: Int): ChunkType? {
return map[x, y]
}
override fun compute(x: Int, y: Int): ChunkType? { override fun compute(x: Int, y: Int): ChunkType? {
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[x, y] ?: create(x, y).also { existing.add(ChunkPos(x, y)); map[x, y] = it; onChunkCreated(it) } return map[x, y] ?: chunkFactory(ChunkPos(x, y)).also { existing.add(ChunkPos(x, y)); map[x, y] = it; onChunkCreated(it) }
}
override fun getCell(x: Int, y: Int): AbstractCell {
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return AbstractCell.NULL
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
return map[ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS]?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: AbstractCell.NULL
}
override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean {
if (!geometry.x.isValidCellIndex(x) || !geometry.y.isValidCellIndex(y)) return false
val ix = geometry.x.cell(x)
val iy = geometry.y.cell(y)
return compute(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS)!!.setCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK, cell)
} }
override fun get(x: Int, y: Int): ChunkType? { override fun get(x: Int, y: Int): ChunkType? {
@ -193,19 +166,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
} }
override fun iterator(): Iterator<ChunkType> { override fun chunks(): List<ChunkType> {
val parent = existing.iterator() return existing.map { (x, y) -> map[x, y] ?: throw ConcurrentModificationException() }
return object : Iterator<ChunkType> {
override fun hasNext(): Boolean {
return parent.hasNext()
}
override fun next(): ChunkType {
val (x, y) = parent.next()
return map[x, y] ?: throw ConcurrentModificationException()
}
}
} }
override val size: Int override val size: Int
@ -291,7 +253,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
entities.values.forEach { it.tick() } entities.values.forEach { it.tick() }
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
for (chunk in chunkMap) for (chunk in chunkMap.chunks())
chunk.tick() chunk.tick()
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()