Merge TicketList into ServerChunk
This commit is contained in:
parent
c3c928de92
commit
a839af1041
@ -1,9 +1,16 @@
|
||||
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 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.vector.Vector2d
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
||||
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
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.IChunkListener
|
||||
import ru.dbotthepony.kstarbound.world.TileHealth
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.api.TileColor
|
||||
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) {
|
||||
/**
|
||||
@ -43,7 +55,236 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
var state: State = State.FRESH
|
||||
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" }
|
||||
|
||||
if (newState >= State.ENTITIES) {
|
||||
@ -51,6 +292,17 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
} else {
|
||||
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> {
|
||||
@ -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 tile = cell.tile(isBackground)
|
||||
|
||||
val params = if (!damage.type.isPenetrating && tile.modifier != null && tile.modifier!!.value.breaksWithTile) {
|
||||
tile.material.value.actualDamageTable + tile.modifier!!.value.actualDamageTable
|
||||
val params = if (!damage.type.isPenetrating && tile.modifier.value.breaksWithTile) {
|
||||
tile.material.value.actualDamageTable + tile.modifier.value.actualDamageTable
|
||||
} else {
|
||||
tile.material.value.actualDamageTable
|
||||
}
|
||||
|
||||
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 (isBackground) {
|
||||
@ -165,6 +417,31 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
@ -178,23 +455,50 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
|
||||
damagedTilesBackground.removeIf { (x, y) ->
|
||||
val health = tileHealthBackground[x, y]
|
||||
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
|
||||
}
|
||||
|
||||
damagedTilesForeground.removeIf { (x, y) ->
|
||||
val health = tileHealthForeground[x, y]
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun legacyNetworkCells(): Object2DArray<LegacyNetworkCellState> {
|
||||
val width = (world.geometry.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE)
|
||||
val height = (world.geometry.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE)
|
||||
fun unload() {
|
||||
if (isUnloaded)
|
||||
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()) {
|
||||
val cells = cells.value
|
||||
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() {
|
||||
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 (y in 0 until height) {
|
||||
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.JsonObject
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.future.asCompletableFuture
|
||||
import kotlinx.coroutines.future.await
|
||||
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.ExecutionSpinner
|
||||
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.WorldGeometry
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
|
||||
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.atomic.AtomicBoolean
|
||||
import java.util.concurrent.locks.LockSupport
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Predicate
|
||||
import java.util.function.Supplier
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class ServerWorld private constructor(
|
||||
val server: StarboundServer,
|
||||
@ -169,8 +157,8 @@ class ServerWorld private constructor(
|
||||
super.close()
|
||||
spinner.unpause()
|
||||
|
||||
ticketListLock.withLock {
|
||||
ticketLists.forEach { it.scope.cancel() }
|
||||
chunkMap.chunks().forEach {
|
||||
it.cancelLoadJob()
|
||||
}
|
||||
|
||||
clients.forEach {
|
||||
@ -289,6 +277,7 @@ class ServerWorld private constructor(
|
||||
|
||||
override fun tick() {
|
||||
super.tick()
|
||||
|
||||
val packet = StepUpdatePacket(ticks)
|
||||
|
||||
clients.forEach {
|
||||
@ -303,10 +292,6 @@ class ServerWorld private constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ticketListLock.withLock {
|
||||
ticketLists.removeIf { it.tick() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun broadcast(packet: IPacket) {
|
||||
@ -337,7 +322,7 @@ class ServerWorld private constructor(
|
||||
}
|
||||
|
||||
private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d {
|
||||
val tickets = ArrayList<ITicket>()
|
||||
val tickets = ArrayList<ServerChunk.ITicket>()
|
||||
|
||||
try {
|
||||
LOGGER.info("Trying to find player spawn position...")
|
||||
@ -424,387 +409,32 @@ class ServerWorld private constructor(
|
||||
return ServerChunk(this, pos)
|
||||
}
|
||||
|
||||
private val ticketMap = Long2ObjectOpenHashMap<TicketList>()
|
||||
private val ticketLists = ArrayList<TicketList>()
|
||||
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): ServerChunk.ITicket? {
|
||||
return chunkMap.compute(pos)?.permanentTicket(target)
|
||||
}
|
||||
|
||||
fun permanentChunkTicket(pos: ChunkPos, target: ServerChunk.State = ServerChunk.State.FULL): ITicket {
|
||||
ticketListLock.withLock {
|
||||
return getTicketList(pos).Ticket(target)
|
||||
}
|
||||
fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITicket> {
|
||||
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
|
||||
}
|
||||
|
||||
fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List<ITicket> {
|
||||
ticketListLock.withLock {
|
||||
return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) }
|
||||
}
|
||||
fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List<ServerChunk.ITicket> {
|
||||
return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull()
|
||||
}
|
||||
|
||||
fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List<ITicket> {
|
||||
ticketListLock.withLock {
|
||||
return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) }
|
||||
}
|
||||
fun temporaryChunkTicket(pos: ChunkPos, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): ServerChunk.ITimedTicket? {
|
||||
return chunkMap.compute(pos)?.temporaryTicket(time, 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" }
|
||||
|
||||
ticketListLock.withLock {
|
||||
return getTicketList(pos).TimedTicket(time, target)
|
||||
}
|
||||
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
|
||||
}
|
||||
|
||||
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" }
|
||||
|
||||
ticketListLock.withLock {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return geometry.region2Chunks(region).map { temporaryChunkTicket(it, time, target) }.filterNotNull()
|
||||
}
|
||||
|
||||
@JsonFactory
|
||||
|
@ -73,7 +73,7 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
|
||||
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) {
|
||||
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) {
|
||||
if (pos !in tickets) {
|
||||
val ticket = world.permanentChunkTicket(pos)
|
||||
val ticket = world.permanentChunkTicket(pos) ?: continue
|
||||
val thisTicket = Ticket(ticket, pos)
|
||||
|
||||
tickets[pos] = thisTicket
|
||||
|
@ -40,7 +40,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
|
||||
var backgroundChangeset = 0
|
||||
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
|
||||
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())
|
||||
|
||||
// TODO: maybe fit them into "width" and "height" variables added recently?
|
||||
protected val cells = lazy {
|
||||
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 {
|
||||
val ix = x and CHUNK_SIZE_FF
|
||||
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 old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL
|
||||
val new = cell.immutable()
|
||||
|
||||
if (old != new) {
|
||||
cells.value[ix, iy] = new
|
||||
cells.value[x, y] = new
|
||||
|
||||
if (old.foreground != new.foreground) {
|
||||
foregroundChanges(ix, iy, new)
|
||||
foregroundChanges(x, y, new)
|
||||
}
|
||||
|
||||
if (old.background != new.background) {
|
||||
backgroundChanges(ix, iy, new)
|
||||
backgroundChanges(x, y, new)
|
||||
}
|
||||
|
||||
if (old.liquid != new.liquid) {
|
||||
liquidChanges(ix, iy, new)
|
||||
liquidChanges(x, y, new)
|
||||
}
|
||||
|
||||
cellChanges(ix, iy, new)
|
||||
cellChanges(x, y, new)
|
||||
}
|
||||
|
||||
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) {
|
||||
changeset++
|
||||
cellChangeset++
|
||||
|
||||
subscribers.forEach { it.onCellChanges(x, y, cell) }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun addListener(subscriber: IChunkListener): Boolean {
|
||||
return subscribers.add(subscriber)
|
||||
}
|
||||
|
||||
fun removeListener(subscriber: IChunkListener): Boolean {
|
||||
return subscribers.remove(subscriber)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
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.IntArraySet
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.arrays.Object2DArray
|
||||
@ -44,6 +46,7 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Predicate
|
||||
import java.util.random.RandomGenerator
|
||||
import java.util.stream.Stream
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val template: WorldTemplate) : ICellAccess, 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 onChunkRemoved(chunk: ChunkType) { }
|
||||
|
||||
abstract inner class ChunkMap : Iterable<ChunkType> {
|
||||
abstract inner class ChunkMap {
|
||||
abstract operator fun get(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)
|
||||
fun remove(pos: ChunkPos) = remove(pos.x, pos.y)
|
||||
|
||||
abstract fun getCell(x: Int, y: Int): AbstractCell
|
||||
abstract fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean
|
||||
private val chunkCache = arrayOfNulls<Chunk<*, *>>(4)
|
||||
|
||||
operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
|
||||
|
||||
protected fun create(x: Int, y: Int): ChunkType {
|
||||
return chunkFactory(ChunkPos(x, y))
|
||||
fun compute(pos: ChunkPos) = compute(pos.x, pos.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
|
||||
@ -95,13 +108,6 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
inner class SparseChunkMap : ChunkMap() {
|
||||
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? {
|
||||
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
|
||||
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? {
|
||||
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
|
||||
|
||||
val index = ChunkPos.toLong(x, y)
|
||||
val get = map[index] ?: create(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)
|
||||
return map[index] ?: chunkFactory(ChunkPos(x, y)).also { map[index] = it; onChunkCreated(it) }
|
||||
}
|
||||
|
||||
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> {
|
||||
return map.values.iterator()
|
||||
override fun chunks(): List<ChunkType> {
|
||||
return ObjectArrayList(map.values)
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
@ -149,29 +140,11 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
|
||||
inner class ArrayChunkMap : ChunkMap() {
|
||||
private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE))
|
||||
private val existing = ObjectArraySet<ChunkPos>()
|
||||
|
||||
private fun getRaw(x: Int, y: Int): ChunkType? {
|
||||
return map[x, y]
|
||||
}
|
||||
private val existing = ObjectAVLTreeSet<ChunkPos>()
|
||||
|
||||
override fun compute(x: Int, y: Int): ChunkType? {
|
||||
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) }
|
||||
}
|
||||
|
||||
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)
|
||||
return map[x, y] ?: chunkFactory(ChunkPos(x, y)).also { existing.add(ChunkPos(x, y)); map[x, y] = it; onChunkCreated(it) }
|
||||
}
|
||||
|
||||
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> {
|
||||
val parent = existing.iterator()
|
||||
|
||||
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 fun chunks(): List<ChunkType> {
|
||||
return existing.map { (x, y) -> map[x, y] ?: throw ConcurrentModificationException() }
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
@ -291,7 +253,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
entities.values.forEach { it.tick() }
|
||||
mailbox.executeQueuedTasks()
|
||||
|
||||
for (chunk in chunkMap)
|
||||
for (chunk in chunkMap.chunks())
|
||||
chunk.tick()
|
||||
|
||||
mailbox.executeQueuedTasks()
|
||||
|
Loading…
Reference in New Issue
Block a user