Merge TicketList into ServerChunk
This commit is contained in:
parent
c3c928de92
commit
a839af1041
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)"
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user