From a839af10418ab615b520ad3f661e5471e455d7ca Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 5 Apr 2024 19:55:45 +0700 Subject: [PATCH] Merge TicketList into ServerChunk --- .../kstarbound/server/world/ServerChunk.kt | 329 +++++++++++++- .../kstarbound/server/world/ServerWorld.kt | 402 +----------------- .../server/world/ServerWorldTracker.kt | 4 +- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 31 +- .../ru/dbotthepony/kstarbound/world/World.kt | 96 ++--- 5 files changed, 373 insertions(+), 489 deletions(-) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 7bb09384..70638958 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -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(world, pos) { /** @@ -43,7 +55,236 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk(Int.MAX_VALUE) + private val permanent = ArrayList() + private val temporary = ObjectAVLTreeSet() + 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 + var listener: IChunkListener? + } + + interface ITimedTicket : ITicket, Comparable { + 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 + val temporary: List + + 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 + val temporary: List + + 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() + + 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 + val temporary: List + + 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 { @@ -85,14 +337,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk 600 + } else { + idleTicks = 0 + } + + if (shouldUnload) { + unload() + return + } + if (state != State.FULL) return @@ -178,23 +455,50 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk 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 { - 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 { 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() + val tickets = ArrayList() 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() - private val ticketLists = ArrayList() - 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 { + return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() } - fun permanentChunkTicket(region: AABBi, target: ServerChunk.State = ServerChunk.State.FULL): List { - ticketListLock.withLock { - return geometry.region2Chunks(region).map { getTicketList(it).Ticket(target) } - } + fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List { + return geometry.region2Chunks(region).map { permanentChunkTicket(it, target) }.filterNotNull() } - fun permanentChunkTicket(region: AABB, target: ServerChunk.State = ServerChunk.State.FULL): List { - 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 { 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 { + fun temporaryChunkTicket(region: AABB, time: Int, target: ServerChunk.State = ServerChunk.State.FULL): List { 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 { - 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 - var listener: IChunkListener? - } - - interface ITimedTicket : ITicket, Comparable { - 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() - private val temporary = ObjectAVLTreeSet() - private var ticks = 0 - private var nextTicketID = 0 - private var isBusy = false - private var chunk by Delegates.notNull() - private val targetState = Channel(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 - val temporary: List - - 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 - val temporary: List - - 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 - val temporary: List - - 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() - - 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index a8dc369e..c1bc52cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 43df2362..1360df53 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -40,7 +40,8 @@ abstract class Chunk, This : Chunk() + 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, This : Chunk, This : Chunk, This : Chunk Unit) { @@ -152,14 +147,6 @@ abstract class Chunk, This : Chunk, ChunkType : Chunk>(val template: WorldTemplate) : ICellAccess, Closeable { @@ -71,21 +74,31 @@ abstract class World, ChunkType : Chunk { + 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 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>(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, ChunkType : Chunk() - 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, ChunkType : Chunk, ChunkType : Chunk { - return map.values.iterator() + override fun chunks(): List { + return ObjectArrayList(map.values) } override val size: Int @@ -149,29 +140,11 @@ abstract class World, ChunkType : Chunk(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE)) - private val existing = ObjectArraySet() - - private fun getRaw(x: Int, y: Int): ChunkType? { - return map[x, y] - } + private val existing = ObjectAVLTreeSet() 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, ChunkType : Chunk { - val parent = existing.iterator() - - return object : Iterator { - 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 { + return existing.map { (x, y) -> map[x, y] ?: throw ConcurrentModificationException() } } override val size: Int @@ -291,7 +253,7 @@ abstract class World, ChunkType : Chunk