From 2b95bf5e3eeceec192aec29db52375134de11c60 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 1 Feb 2024 18:33:11 +0700 Subject: [PATCH] More work on chunk map and chunk tickets --- .../kstarbound/client/StarboundClient.kt | 2 +- .../network/packets/TrackedPositionPacket.kt | 4 +- .../ru/dbotthepony/kstarbound/io/BTreeDB.kt | 238 +++++++++--------- .../kstarbound/server/world/IChunkSource.kt | 4 +- .../server/world/LegacyChunkSource.kt | 72 ++++-- .../kstarbound/server/world/ServerChunk.kt | 1 + .../kstarbound/server/world/ServerWorld.kt | 31 ++- .../ru/dbotthepony/kstarbound/world/Chunk.kt | 99 ++++++-- .../kstarbound/world/IChunkSubscriber.kt | 11 + .../ru/dbotthepony/kstarbound/world/World.kt | 58 ++++- .../kstarbound/world/entities/WorldObject.kt | 33 ++- 11 files changed, 363 insertions(+), 190 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/IChunkSubscriber.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 03f9add4..a1fde66a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -1004,7 +1004,7 @@ class StarboundClient : Closeable { if (activeConnection != null) { activeConnection.send(TrackedPositionPacket(camera.pos)) - activeConnection.send(TrackedSizePacket(12, 12)) + activeConnection.send(TrackedSizePacket(2, 2)) } uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt index 8795198d..e349fdf2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/TrackedPositionPacket.kt @@ -24,8 +24,8 @@ data class TrackedSizePacket(val width: Int, val height: Int) : IServerPacket { constructor(stream: DataInputStream) : this(stream.readUnsignedByte(), stream.readUnsignedByte()) init { - require(width in 0 .. 12) { "Too big chunk width to track: $width" } - require(height in 0 .. 12) { "Too big chunk height to track: $height" } + require(width in 1 .. 12) { "Bad chunk width to track: $width" } + require(height in 1 .. 12) { "Bad chunk height to track: $height" } } override fun write(stream: DataOutputStream) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB.kt index 01d76cc6..59eb924b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/BTreeDB.kt @@ -3,6 +3,8 @@ package ru.dbotthepony.kstarbound.io import it.unimi.dsi.fastutil.ints.IntArraySet import java.io.* import java.util.* +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock private fun readHeader(reader: RandomAccessFile, required: Char) { val read = reader.read() @@ -47,6 +49,7 @@ private operator fun ByteArray.compareTo(b: ByteArray): Int { */ class BTreeDB(val path: File) { val reader = RandomAccessFile(path, "r") + private val lock = ReentrantLock() init { readHeader(reader, 'B') @@ -86,167 +89,174 @@ class BTreeDB(val path: File) { val rootNodeIndex get() = if (useNodeTwo) rootNode2Index else rootNode1Index val rootNodeIsLeaf get() = if (useNodeTwo) rootNode2IsLeaf else rootNode1IsLeaf - fun readBlockType() = TreeBlockType[reader.readString(2)] + fun readBlockType() = lock.withLock { TreeBlockType[reader.readString(2)] } fun findAllKeys(index: Long = rootNodeIndex): List { - seekBlock(index) + lock.withLock { + seekBlock(index) - val list = ArrayList() - val type = readBlockType() + val list = ArrayList() + val type = readBlockType() - if (type == TreeBlockType.LEAF) { - val keyAmount = reader.readInt() - // offset внутри лепестка в байтах - var offset = 6 + if (type == TreeBlockType.LEAF) { + val keyAmount = reader.readInt() + // offset внутри лепестка в байтах + var offset = 6 - for (i in 0 until keyAmount) { - // читаем ключ - list.add(ByteArray(indexKeySize).also { reader.read(it) }) - offset += indexKeySize + for (i in 0 until keyAmount) { + // читаем ключ + list.add(ByteArray(indexKeySize).also { reader.read(it) }) + offset += indexKeySize - // читаем размер данных внутри ключа - var (dataLength, readBytes) = reader.readVarIntInfo() - offset += readBytes + // читаем размер данных внутри ключа + var (dataLength, readBytes) = reader.readVarIntInfo() + offset += readBytes - while (true) { - // если конец данных внутри текущего блока, останавливаемся - if (offset + dataLength <= blockSize - 4) { - reader.skipBytes(dataLength) - offset += dataLength - break + while (true) { + // если конец данных внутри текущего блока, останавливаемся + if (offset + dataLength <= blockSize - 4) { + reader.skipBytes(dataLength) + offset += dataLength + break + } + + // иначе, ищем следующий блок + + // пропускаем оставшиеся данные, переходим на границу текущего блока-лепестка + val delta = (blockSize - 4 - offset) + reader.skipBytes(delta) + + // ищем следующий блок с нашими данными + val nextBlockIndex = reader.readInt() + seekBlock(nextBlockIndex.toLong()) + + // удостоверяемся что мы попали в лепесток + check(readBlockType() == TreeBlockType.LEAF) { "Did not hit leaf block" } + offset = 2 + dataLength -= delta } - - // иначе, ищем следующий блок - - // пропускаем оставшиеся данные, переходим на границу текущего блока-лепестка - val delta = (blockSize - 4 - offset) - reader.skipBytes(delta) - - // ищем следующий блок с нашими данными - val nextBlockIndex = reader.readInt() - seekBlock(nextBlockIndex.toLong()) - - // удостоверяемся что мы попали в лепесток - check(readBlockType() == TreeBlockType.LEAF) { "Did not hit leaf block" } - offset = 2 - dataLength -= delta } - } - } else if (type == TreeBlockType.INDEX) { - reader.skipBytes(1) - val keyAmount = reader.readInt() + } else if (type == TreeBlockType.INDEX) { + reader.skipBytes(1) + val keyAmount = reader.readInt() - val blockList = IntArraySet() - blockList.add(reader.readInt()) - - for (i in 0 until keyAmount) { - // ключ - reader.skipBytes(indexKeySize) - - // указатель на блок + val blockList = IntArraySet() blockList.add(reader.readInt()) - } - // читаем все дочерние блоки на ключи - for (block in blockList.intIterator()) { - for (key in findAllKeys(block.toLong())) { - list.add(key) + for (i in 0 until keyAmount) { + // ключ + reader.skipBytes(indexKeySize) + + // указатель на блок + blockList.add(reader.readInt()) + } + + // читаем все дочерние блоки на ключи + for (block in blockList.intIterator()) { + for (key in findAllKeys(block.toLong())) { + list.add(key) + } } } - } - return list + return list + } } fun read(key: ByteArray): ByteArray? { require(key.size == indexKeySize) { "Key provided is ${key.size} in size, while $indexKeySize is required" } - seekBlock(rootNodeIndex) - var type = readBlockType() - var iterations = 1000 + lock.withLock { + seekBlock(rootNodeIndex) + var type = readBlockType() + var iterations = 1000 - val keyLoader = ByteArray(indexKeySize) + val keyLoader = ByteArray(indexKeySize) - // сканирование индекса - while (iterations-- > 0 && type != TreeBlockType.LEAF) { - if (type == TreeBlockType.FREE) { - throw IllegalStateException("Hit free block while scanning index for ${key.joinToString(", ")}") - } + // сканирование индекса + while (iterations-- > 0 && type != TreeBlockType.LEAF) { + if (type == TreeBlockType.FREE) { + throw IllegalStateException("Hit free block while scanning index for ${key.joinToString(", ")}") + } - reader.skipBytes(1) + reader.skipBytes(1) - val keyCount = reader.readInt() - // if keyAmount == 4 then - // B a B b B c B d B - val readKeys = ByteArray((keyCount + 1) * 4 + keyCount * indexKeySize) - reader.readFully(readKeys) + val keyCount = reader.readInt() + // if keyAmount == 4 then + // B a B b B c B d B + val readKeys = ByteArray((keyCount + 1) * 4 + keyCount * indexKeySize) + reader.readFully(readKeys) - val stream = DataInputStream(ByteArrayInputStream(readKeys)) + val stream = DataInputStream(ByteArrayInputStream(readKeys)) - var read = false + var read = false - // B a - // B b - // B c - // B d - for (keyIndex in 0 until keyCount) { - // указатель на левый блок - val pointer = stream.readInt() + // B a + // B b + // B c + // B d + for (keyIndex in 0 until keyCount) { + // указатель на левый блок + val pointer = stream.readInt() - // левый ключ, всё что меньше него находится в левом блоке - stream.readFully(keyLoader) + // левый ключ, всё что меньше него находится в левом блоке + stream.readFully(keyLoader) - // нужный ключ меньше самого первого ключа, поэтому он находится где то в левом блоке - if (key < keyLoader) { - seekBlock(pointer.toLong()) + // нужный ключ меньше самого первого ключа, поэтому он находится где то в левом блоке + if (key < keyLoader) { + seekBlock(pointer.toLong()) + type = readBlockType() + read = true + break + } + } + + if (!read) { + // ... B + seekBlock(stream.readInt().toLong()) type = readBlockType() - read = true - break } } - if (!read) { - // ... B - seekBlock(stream.readInt().toLong()) - type = readBlockType() - } - } + // мы пришли в лепесток, теперь прямолинейно ищем в linked list + val leafStream = DataInputStream(BufferedInputStream(LeafInputStream(2))) + val keyCount = leafStream.readInt() - // мы пришли в лепесток, теперь прямолинейно ищем в linked list - val leafStream = DataInputStream(BufferedInputStream(LeafInputStream(2))) - val keyCount = leafStream.readInt() + for (keyIndex in 0 until keyCount) { + // читаем ключ + leafStream.read(keyLoader) - for (keyIndex in 0 until keyCount) { - // читаем ключ - leafStream.read(keyLoader) + // читаем размер данных + val dataLength = leafStream.readVarInt() - // читаем размер данных - val dataLength = leafStream.readVarInt() + // это наш блок + if (keyLoader.contentEquals(key)) { + val binary = ByteArray(dataLength) - // это наш блок - if (keyLoader.contentEquals(key)) { - val binary = ByteArray(dataLength) + if (dataLength == 0) { + // нет данных (?) + return binary + } + + leafStream.readFully(binary) - if (dataLength == 0) { - // нет данных (?) return binary + } else { + leafStream.skipBytes(dataLength) } - - leafStream.readFully(binary) - - return binary - } else { - leafStream.skipBytes(dataLength) } - } - return null + return null + } } fun seekBlock(id: Long) { require(id >= 0) { "Negative id $id" } require(id * blockSize + blocksOffsetStart < reader.length()) { "Tried to seek block with $id, but it is outside of file's bounds (file size ${reader.length()} bytes, seeking ${id * blockSize + blocksOffsetStart})! (does not exist)" } - reader.seek(id * blockSize + blocksOffsetStart) + + lock.withLock { + reader.seek(id * blockSize + blocksOffsetStart) + } } private inner class LeafInputStream(private var offset: Int) : InputStream() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt index 75dc4cd3..9e4ab34b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/IChunkSource.kt @@ -10,14 +10,14 @@ import java.util.concurrent.CompletableFuture interface IChunkSource { fun getTiles(pos: ChunkPos): CompletableFuture>> - fun getObjects(pos: ChunkPos): CompletableFuture>> + fun getEntities(pos: ChunkPos): CompletableFuture>> object Void : IChunkSource { override fun getTiles(pos: ChunkPos): CompletableFuture>> { return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY))) } - override fun getObjects(pos: ChunkPos): CompletableFuture>> { + override fun getEntities(pos: ChunkPos): CompletableFuture>> { return CompletableFuture.completedFuture(KOptional.of(emptyList())) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt index 12904081..e1b4ddea 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt @@ -1,13 +1,20 @@ package ru.dbotthepony.kstarbound.server.world +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.io.BTreeDB +import ru.dbotthepony.kstarbound.io.readVarInt +import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.util.KOptional +import ru.dbotthepony.kstarbound.util.get import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kvector.arrays.Object2DArray +import ru.dbotthepony.kvector.vector.Vector2i import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.DataInputStream @@ -17,26 +24,63 @@ import java.util.zip.InflaterInputStream class LegacyChunkSource(val db: BTreeDB) : IChunkSource { override fun getTiles(pos: ChunkPos): CompletableFuture>> { - val chunkX = pos.x - val chunkY = pos.y - val key = byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) - val data = db.read(key) ?: return CompletableFuture.completedFuture(KOptional.empty()) + return CompletableFuture.supplyAsync { + val chunkX = pos.x + val chunkY = pos.y + val key = byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) + val data = db.read(key) ?: return@supplyAsync KOptional.empty() - val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) - reader.skipBytes(3) + val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) + reader.skipBytes(3) - val result = Object2DArray.nulls(CHUNK_SIZE, CHUNK_SIZE) + val result = Object2DArray.nulls(CHUNK_SIZE, CHUNK_SIZE) - for (y in 0 until CHUNK_SIZE) { - for (x in 0 until CHUNK_SIZE) { - result[x, y] = MutableCell().read(reader) + for (y in 0 until CHUNK_SIZE) { + for (x in 0 until CHUNK_SIZE) { + result[x, y] = MutableCell().read(reader) + } } - } - return CompletableFuture.completedFuture(KOptional(result as Object2DArray)) + KOptional(result as Object2DArray) + } } - override fun getObjects(pos: ChunkPos): CompletableFuture>> { - return CompletableFuture.completedFuture(KOptional.of(listOf())) + override fun getEntities(pos: ChunkPos): CompletableFuture>> { + return CompletableFuture.supplyAsync { + val chunkX = pos.x + val chunkY = pos.y + val key = byteArrayOf(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) + val data = db.read(key) ?: return@supplyAsync KOptional.empty() + + val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater()))) + val i = reader.readVarInt() + val objects = ArrayList() + + for (i2 in 0 until i) { + val obj = VersionedJson(reader) + + if (obj.identifier == "ObjectEntity") { + try { + val content = obj.content.asJsonObject + val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'") + val pos = content.get("tilePosition", vectors) { throw IllegalArgumentException("No tilePosition was present in saved data") } + val result = WorldObject(prototype, pos) + result.deserialize(content) + objects.add(result) + } catch (err: Throwable) { + LOGGER.error("Unable to deserialize entity in chunk $pos", err) + } + } else { + LOGGER.error("Unknown entity type in chunk $pos: ${obj.identifier}") + } + } + + return@supplyAsync KOptional(objects) + } + } + + companion object { + private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } + private val LOGGER = LogManager.getLogger() } } 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 bb809901..c43847df 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.server.world +import it.unimi.dsi.fastutil.objects.ObjectArraySet import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index b54a0ca3..39a73dba 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -10,11 +10,13 @@ import ru.dbotthepony.kstarbound.server.network.ServerPlayer import ru.dbotthepony.kstarbound.util.KOptional import ru.dbotthepony.kstarbound.util.composeFutures import ru.dbotthepony.kstarbound.world.ChunkPos +import ru.dbotthepony.kstarbound.world.IChunkSubscriber import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.WorldGeometry -import ru.dbotthepony.kstarbound.world.api.AbstractCell -import ru.dbotthepony.kvector.arrays.Object2DArray +import ru.dbotthepony.kstarbound.world.entities.Entity +import ru.dbotthepony.kstarbound.world.entities.WorldObject import java.io.Closeable +import java.util.concurrent.CompletableFuture import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.LockSupport import java.util.function.Consumer @@ -196,14 +198,25 @@ class ServerWorld( ticketLists.add(this@TicketList) if (chunkProviders.isNotEmpty()) { - val onFinish = Consumer>> { - if (isValid && it.isPresent) { - val chunk = chunkMap.compute(pos) ?: return@Consumer - chunk.loadCells(it.value) - } - } + composeFutures(chunkProviders) + { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) } + .thenAccept(Consumer { tiles -> + if (!isValid || !tiles.isPresent) return@Consumer - composeFutures(chunkProviders) { it.getTiles(pos) }.thenAcceptAsync(onFinish, mailbox) + composeFutures(chunkProviders) + { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getEntities(pos) } + .thenAcceptAsync(Consumer { ents -> + if (!isValid) return@Consumer + val chunk = chunkMap.compute(pos) ?: return@Consumer + chunk.loadCells(tiles.value) + + ents.ifPresent { + for (obj in it) { + chunk.addObject(obj) + } + } + }, mailbox) + }) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index e5ea3389..6719d2bd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.world +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ICellAccess @@ -7,6 +9,7 @@ import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.Entity +import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.Vector2d @@ -41,6 +44,25 @@ abstract class Chunk, This : Chunk() + protected val objects = ReferenceOpenHashSet() + protected val subscribers = ObjectArraySet() + + // local cells' tile access + val localBackgroundView = TileView.Background(this) + val localForegroundView = TileView.Foreground(this) + + // relative world cells access (accessing 0, 0 will lookup cell in world, relative to this chunk) + val worldView = OffsetCellAccess(world, pos.x * CHUNK_SIZE, pos.y * CHUNK_SIZE) + val worldBackgroundView = TileView.Background(worldView) + val worldForegroundView = TileView.Foreground(worldView) + + val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) + + protected val cells = lazy { + Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL) + } + fun loadCells(source: Object2DArray) { val ours = cells.value source.checkSizeEquals(ours) @@ -52,10 +74,6 @@ abstract class Chunk, This : Chunk, This : Chunk, This : Chunk() + fun addSubscriber(subscriber: IChunkSubscriber) { + subscribers.add(subscriber) + } - protected open fun onEntityAdded(entity: Entity) { } - protected open fun onEntityTransferedToThis(entity: Entity, otherChunk: This) { } - protected open fun onEntityTransferedFromThis(entity: Entity, otherChunk: This) { } - protected open fun onEntityRemoved(entity: Entity) { } + fun removeSubscriber(subscriber: IChunkSubscriber) { + subscribers.remove(subscriber) + } fun addEntity(entity: Entity) { world.lock.withLock { @@ -159,7 +167,7 @@ abstract class Chunk, This : Chunk, This : Chunk, This : Chunk, This : Chunk, ChunkType : Chunk { 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) @@ -94,6 +95,8 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk { + return map.values.iterator() + } + + override val size: Int + get() = map.size } inner class ArrayChunkMap : ChunkMap() { private val map = Object2DArray.nulls(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] @@ -154,7 +167,7 @@ abstract class World, ChunkType : Chunk, 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 val size: Int + get() = existing.size } val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap() @@ -193,7 +234,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, val prototype: Registry.Entry, val pos: Vector2i, ) : JsonDriven(prototype.file?.computeDirectory() ?: "/") { - constructor(world: World<*, *>, data: JsonObject) : this( - world, - Registries.worldObjects[data["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${data["name"]}'"), - data.get("tilePosition", vectors) { throw IllegalArgumentException("No tilePosition was present in saved data") } - ) { + fun deserialize(data: JsonObject) { direction = data.get("direction", directions) { Side.LEFT } orientationIndex = data.get("orientationIndex", -1) interactive = data.get("interactive", false) @@ -49,13 +46,16 @@ open class WorldObject( } } - val mailbox = MailboxExecutorService(world.mailbox.thread) + val mailbox = MailboxExecutorService() + var world: World<*, *> by Delegates.notNull() + private set // // internal runtime properties // - val clientWorld get() = world as ClientWorld - val orientations = prototype.value.orientations + inline val clientWorld get() = world as ClientWorld + inline val serverWorld get() = world as ServerWorld + inline val orientations get() = prototype.value.orientations protected val renderParamLocations = Object2ObjectOpenHashMap String?>() private var frame = 0 set(value) { @@ -127,18 +127,15 @@ open class WorldObject( protected open fun innerSpawn() {} protected open fun innerRemove() {} - fun spawn() { - if (isSpawned) return + fun spawn(world: World<*, *>) { + check(!isSpawned) { "Already spawned in ${this.world}!" } + this.world = world isSpawned = true - - world.mailbox.execute { - world.objects.add(this) - innerSpawn() - invalidate() - } + innerSpawn() + invalidate() } - open fun remove() { + fun remove() { if (isRemoved || !isSpawned) return isRemoved = true