More work on chunk map and chunk tickets

This commit is contained in:
DBotThePony 2024-02-01 18:33:11 +07:00
parent a028694010
commit 2b95bf5e3e
Signed by: DBot
GPG Key ID: DCC23B5715498507
11 changed files with 363 additions and 190 deletions

View File

@ -1004,7 +1004,7 @@ class StarboundClient : Closeable {
if (activeConnection != null) { if (activeConnection != null) {
activeConnection.send(TrackedPositionPacket(camera.pos)) activeConnection.send(TrackedPositionPacket(camera.pos))
activeConnection.send(TrackedSizePacket(12, 12)) activeConnection.send(TrackedSizePacket(2, 2))
} }
uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen } uberShaderPrograms.forEachValid { it.viewMatrix = viewportMatrixScreen }

View File

@ -24,8 +24,8 @@ data class TrackedSizePacket(val width: Int, val height: Int) : IServerPacket {
constructor(stream: DataInputStream) : this(stream.readUnsignedByte(), stream.readUnsignedByte()) constructor(stream: DataInputStream) : this(stream.readUnsignedByte(), stream.readUnsignedByte())
init { init {
require(width in 0 .. 12) { "Too big chunk width to track: $width" } require(width in 1 .. 12) { "Bad chunk width to track: $width" }
require(height in 0 .. 12) { "Too big chunk height to track: $height" } require(height in 1 .. 12) { "Bad chunk height to track: $height" }
} }
override fun write(stream: DataOutputStream) { override fun write(stream: DataOutputStream) {

View File

@ -3,6 +3,8 @@ package ru.dbotthepony.kstarbound.io
import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.ints.IntArraySet
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
private fun readHeader(reader: RandomAccessFile, required: Char) { private fun readHeader(reader: RandomAccessFile, required: Char) {
val read = reader.read() val read = reader.read()
@ -47,6 +49,7 @@ private operator fun ByteArray.compareTo(b: ByteArray): Int {
*/ */
class BTreeDB(val path: File) { class BTreeDB(val path: File) {
val reader = RandomAccessFile(path, "r") val reader = RandomAccessFile(path, "r")
private val lock = ReentrantLock()
init { init {
readHeader(reader, 'B') readHeader(reader, 'B')
@ -86,167 +89,174 @@ class BTreeDB(val path: File) {
val rootNodeIndex get() = if (useNodeTwo) rootNode2Index else rootNode1Index val rootNodeIndex get() = if (useNodeTwo) rootNode2Index else rootNode1Index
val rootNodeIsLeaf get() = if (useNodeTwo) rootNode2IsLeaf else rootNode1IsLeaf 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<ByteArray> { fun findAllKeys(index: Long = rootNodeIndex): List<ByteArray> {
seekBlock(index) lock.withLock {
seekBlock(index)
val list = ArrayList<ByteArray>() val list = ArrayList<ByteArray>()
val type = readBlockType() val type = readBlockType()
if (type == TreeBlockType.LEAF) { if (type == TreeBlockType.LEAF) {
val keyAmount = reader.readInt() val keyAmount = reader.readInt()
// offset внутри лепестка в байтах // offset внутри лепестка в байтах
var offset = 6 var offset = 6
for (i in 0 until keyAmount) { for (i in 0 until keyAmount) {
// читаем ключ // читаем ключ
list.add(ByteArray(indexKeySize).also { reader.read(it) }) list.add(ByteArray(indexKeySize).also { reader.read(it) })
offset += indexKeySize offset += indexKeySize
// читаем размер данных внутри ключа // читаем размер данных внутри ключа
var (dataLength, readBytes) = reader.readVarIntInfo() var (dataLength, readBytes) = reader.readVarIntInfo()
offset += readBytes offset += readBytes
while (true) { while (true) {
// если конец данных внутри текущего блока, останавливаемся // если конец данных внутри текущего блока, останавливаемся
if (offset + dataLength <= blockSize - 4) { if (offset + dataLength <= blockSize - 4) {
reader.skipBytes(dataLength) reader.skipBytes(dataLength)
offset += dataLength offset += dataLength
break 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) {
} else if (type == TreeBlockType.INDEX) { reader.skipBytes(1)
reader.skipBytes(1) val keyAmount = reader.readInt()
val keyAmount = reader.readInt()
val blockList = IntArraySet() val blockList = IntArraySet()
blockList.add(reader.readInt())
for (i in 0 until keyAmount) {
// ключ
reader.skipBytes(indexKeySize)
// указатель на блок
blockList.add(reader.readInt()) blockList.add(reader.readInt())
}
// читаем все дочерние блоки на ключи for (i in 0 until keyAmount) {
for (block in blockList.intIterator()) { // ключ
for (key in findAllKeys(block.toLong())) { reader.skipBytes(indexKeySize)
list.add(key)
// указатель на блок
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? { fun read(key: ByteArray): ByteArray? {
require(key.size == indexKeySize) { "Key provided is ${key.size} in size, while $indexKeySize is required" } require(key.size == indexKeySize) { "Key provided is ${key.size} in size, while $indexKeySize is required" }
seekBlock(rootNodeIndex) lock.withLock {
var type = readBlockType() seekBlock(rootNodeIndex)
var iterations = 1000 var type = readBlockType()
var iterations = 1000
val keyLoader = ByteArray(indexKeySize) val keyLoader = ByteArray(indexKeySize)
// сканирование индекса // сканирование индекса
while (iterations-- > 0 && type != TreeBlockType.LEAF) { while (iterations-- > 0 && type != TreeBlockType.LEAF) {
if (type == TreeBlockType.FREE) { if (type == TreeBlockType.FREE) {
throw IllegalStateException("Hit free block while scanning index for ${key.joinToString(", ")}") throw IllegalStateException("Hit free block while scanning index for ${key.joinToString(", ")}")
} }
reader.skipBytes(1) reader.skipBytes(1)
val keyCount = reader.readInt() val keyCount = reader.readInt()
// if keyAmount == 4 then // if keyAmount == 4 then
// B a B b B c B d B // B a B b B c B d B
val readKeys = ByteArray((keyCount + 1) * 4 + keyCount * indexKeySize) val readKeys = ByteArray((keyCount + 1) * 4 + keyCount * indexKeySize)
reader.readFully(readKeys) reader.readFully(readKeys)
val stream = DataInputStream(ByteArrayInputStream(readKeys)) val stream = DataInputStream(ByteArrayInputStream(readKeys))
var read = false var read = false
// B a // B a
// B b // B b
// B c // B c
// B d // B d
for (keyIndex in 0 until keyCount) { for (keyIndex in 0 until keyCount) {
// указатель на левый блок // указатель на левый блок
val pointer = stream.readInt() val pointer = stream.readInt()
// левый ключ, всё что меньше него находится в левом блоке // левый ключ, всё что меньше него находится в левом блоке
stream.readFully(keyLoader) stream.readFully(keyLoader)
// нужный ключ меньше самого первого ключа, поэтому он находится где то в левом блоке // нужный ключ меньше самого первого ключа, поэтому он находится где то в левом блоке
if (key < keyLoader) { if (key < keyLoader) {
seekBlock(pointer.toLong()) seekBlock(pointer.toLong())
type = readBlockType()
read = true
break
}
}
if (!read) {
// ... B
seekBlock(stream.readInt().toLong())
type = readBlockType() type = readBlockType()
read = true
break
} }
} }
if (!read) { // мы пришли в лепесток, теперь прямолинейно ищем в linked list
// ... B val leafStream = DataInputStream(BufferedInputStream(LeafInputStream(2)))
seekBlock(stream.readInt().toLong()) val keyCount = leafStream.readInt()
type = readBlockType()
}
}
// мы пришли в лепесток, теперь прямолинейно ищем в linked list for (keyIndex in 0 until keyCount) {
val leafStream = DataInputStream(BufferedInputStream(LeafInputStream(2))) // читаем ключ
val keyCount = leafStream.readInt() leafStream.read(keyLoader)
for (keyIndex in 0 until keyCount) { // читаем размер данных
// читаем ключ val dataLength = leafStream.readVarInt()
leafStream.read(keyLoader)
// читаем размер данных // это наш блок
val dataLength = leafStream.readVarInt() if (keyLoader.contentEquals(key)) {
val binary = ByteArray(dataLength)
// это наш блок if (dataLength == 0) {
if (keyLoader.contentEquals(key)) { // нет данных (?)
val binary = ByteArray(dataLength) return binary
}
leafStream.readFully(binary)
if (dataLength == 0) {
// нет данных (?)
return binary return binary
} else {
leafStream.skipBytes(dataLength)
} }
leafStream.readFully(binary)
return binary
} else {
leafStream.skipBytes(dataLength)
} }
}
return null return null
}
} }
fun seekBlock(id: Long) { fun seekBlock(id: Long) {
require(id >= 0) { "Negative id $id" } 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)" } 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() { private inner class LeafInputStream(private var offset: Int) : InputStream() {

View File

@ -10,14 +10,14 @@ import java.util.concurrent.CompletableFuture
interface IChunkSource { interface IChunkSource {
fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>>
fun getObjects(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>>
object Void : IChunkSource { object Void : IChunkSource {
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> { override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY))) return CompletableFuture.completedFuture(KOptional.of(Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.EMPTY)))
} }
override fun getObjects(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> { override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> {
return CompletableFuture.completedFuture(KOptional.of(emptyList())) return CompletableFuture.completedFuture(KOptional.of(emptyList()))
} }
} }

View File

@ -1,13 +1,20 @@
package ru.dbotthepony.kstarbound.server.world 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.BTreeDB
import ru.dbotthepony.kstarbound.io.readVarInt
import ru.dbotthepony.kstarbound.json.VersionedJson
import ru.dbotthepony.kstarbound.util.KOptional import ru.dbotthepony.kstarbound.util.KOptional
import ru.dbotthepony.kstarbound.util.get
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.api.MutableCell
import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.DataInputStream import java.io.DataInputStream
@ -17,26 +24,63 @@ import java.util.zip.InflaterInputStream
class LegacyChunkSource(val db: BTreeDB) : IChunkSource { class LegacyChunkSource(val db: BTreeDB) : IChunkSource {
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> { override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
val chunkX = pos.x return CompletableFuture.supplyAsync {
val chunkY = pos.y val chunkX = pos.x
val key = byteArrayOf(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) val chunkY = pos.y
val data = db.read(key) ?: return CompletableFuture.completedFuture(KOptional.empty()) 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()))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(data), Inflater())))
reader.skipBytes(3) reader.skipBytes(3)
val result = Object2DArray.nulls<MutableCell>(CHUNK_SIZE, CHUNK_SIZE) val result = Object2DArray.nulls<MutableCell>(CHUNK_SIZE, CHUNK_SIZE)
for (y in 0 until CHUNK_SIZE) { for (y in 0 until CHUNK_SIZE) {
for (x in 0 until CHUNK_SIZE) { for (x in 0 until CHUNK_SIZE) {
result[x, y] = MutableCell().read(reader) result[x, y] = MutableCell().read(reader)
}
} }
}
return CompletableFuture.completedFuture(KOptional(result as Object2DArray<out AbstractCell>)) KOptional(result as Object2DArray<out AbstractCell>)
}
} }
override fun getObjects(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> { override fun getEntities(pos: ChunkPos): CompletableFuture<KOptional<Collection<WorldObject>>> {
return CompletableFuture.completedFuture(KOptional.of(listOf())) 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<WorldObject>()
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()
} }
} }

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.server.world 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_SIZE
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos

View File

@ -10,11 +10,13 @@ import ru.dbotthepony.kstarbound.server.network.ServerPlayer
import ru.dbotthepony.kstarbound.util.KOptional import ru.dbotthepony.kstarbound.util.KOptional
import ru.dbotthepony.kstarbound.util.composeFutures import ru.dbotthepony.kstarbound.util.composeFutures
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkSubscriber
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.io.Closeable import java.io.Closeable
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.function.Consumer import java.util.function.Consumer
@ -196,14 +198,25 @@ class ServerWorld(
ticketLists.add(this@TicketList) ticketLists.add(this@TicketList)
if (chunkProviders.isNotEmpty()) { if (chunkProviders.isNotEmpty()) {
val onFinish = Consumer<KOptional<Object2DArray<out AbstractCell>>> { composeFutures(chunkProviders)
if (isValid && it.isPresent) { { if (!isValid) CompletableFuture.completedFuture(KOptional.empty()) else it.getTiles(pos) }
val chunk = chunkMap.compute(pos) ?: return@Consumer .thenAccept(Consumer { tiles ->
chunk.loadCells(it.value) 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)
})
} }
} }
} }

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world 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 it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
import ru.dbotthepony.kstarbound.world.api.ICellAccess 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.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
@ -41,6 +44,25 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
protected val entities = ReferenceOpenHashSet<Entity>()
protected val objects = ReferenceOpenHashSet<WorldObject>()
protected val subscribers = ObjectArraySet<IChunkSubscriber>()
// 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<out AbstractCell>) { fun loadCells(source: Object2DArray<out AbstractCell>) {
val ours = cells.value val ours = cells.value
source.checkSizeEquals(ours) source.checkSizeEquals(ours)
@ -52,10 +74,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
} }
protected val cells = lazy {
Object2DArray(CHUNK_SIZE, CHUNK_SIZE, AbstractCell.NULL)
}
override fun getCell(x: Int, y: Int): AbstractCell { override fun getCell(x: Int, y: Int): AbstractCell {
if (!cells.isInitialized()) if (!cells.isInitialized())
return AbstractCell.NULL return AbstractCell.NULL
@ -97,17 +115,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return true return true
} }
// 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 open fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) { protected open fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) {
cellChanges(x, y, cell) cellChanges(x, y, cell)
tileChangeset++ tileChangeset++
@ -145,12 +152,13 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return world.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS) return world.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS)
} }
protected val entities = ReferenceOpenHashSet<Entity>() fun addSubscriber(subscriber: IChunkSubscriber) {
subscribers.add(subscriber)
}
protected open fun onEntityAdded(entity: Entity) { } fun removeSubscriber(subscriber: IChunkSubscriber) {
protected open fun onEntityTransferedToThis(entity: Entity, otherChunk: This) { } subscribers.remove(subscriber)
protected open fun onEntityTransferedFromThis(entity: Entity, otherChunk: This) { } }
protected open fun onEntityRemoved(entity: Entity) { }
fun addEntity(entity: Entity) { fun addEntity(entity: Entity) {
world.lock.withLock { world.lock.withLock {
@ -159,7 +167,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
changeset++ changeset++
onEntityAdded(entity) subscribers.forEach { it.onEntityAdded(entity) }
} }
} }
@ -177,8 +185,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
changeset++ changeset++
onEntityTransferedToThis(entity, otherChunk as This) otherChunk.subscribers.forEach { it.onEntityRemoved(entity) }
otherChunk.onEntityTransferedFromThis(entity, this as This) subscribers.forEach { it.onEntityAdded(entity) }
if (!otherChunk.entities.remove(entity)) { if (!otherChunk.entities.remove(entity)) {
throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer")
@ -193,7 +201,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
} }
changeset++ changeset++
onEntityRemoved(entity) subscribers.forEach { it.onEntityRemoved(entity) }
} }
} }
@ -201,6 +209,49 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)" return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)"
} }
fun addObject(obj: WorldObject) {
world.lock.withLock {
if (!objects.add(obj))
throw IllegalStateException("$this already has object $obj!")
if (!world.objects.add(obj))
throw IllegalStateException("World $world already has object $obj!")
obj.spawn(world)
subscribers.forEach { it.onObjectAdded(obj) }
}
}
fun removeObject(obj: WorldObject) {
world.lock.withLock {
if (!objects.remove(obj))
throw IllegalStateException("$this does not have object $obj!")
if (!world.objects.remove(obj))
throw IllegalStateException("World $world does not have object $obj!")
subscribers.forEach { it.onObjectRemoved(obj) }
}
}
open fun remove() {
world.lock.withLock {
for (obj in ObjectArrayList(objects)) {
if (!world.objects.remove(obj)) {
throw IllegalStateException("World $world does not have object $obj!")
}
}
for (ent in ObjectArrayList(entities)) {
ent.chunk = null
}
}
}
open fun think() {
}
companion object { companion object {
private val aabbBase = AABB( private val aabbBase = AABB(
Vector2d.ZERO, Vector2d.ZERO,

View File

@ -0,0 +1,11 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
interface IChunkSubscriber {
fun onEntityAdded(entity: Entity)
fun onEntityRemoved(entity: Entity)
fun onObjectAdded(obj: WorldObject)
fun onObjectRemoved(obj: WorldObject)
}

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
@ -62,7 +63,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return chunkMap.setCell(x, y, cell) return chunkMap.setCell(x, y, cell)
} }
abstract inner class ChunkMap { abstract inner class ChunkMap : Iterable<ChunkType> {
abstract operator fun get(x: Int, y: Int): ChunkType? abstract operator fun get(x: Int, y: Int): ChunkType?
abstract fun compute(x: Int, y: Int): ChunkType? abstract fun compute(x: Int, y: Int): ChunkType?
fun compute(pos: ChunkPos) = compute(pos.x, pos.y) fun compute(pos: ChunkPos) = compute(pos.x, pos.y)
@ -94,6 +95,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
return chunk return chunk
} }
abstract val size: Int
} }
// around 30% slower than ArrayChunkMap, but can support insanely large worlds // around 30% slower than ArrayChunkMap, but can support insanely large worlds
@ -141,12 +144,22 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
map.remove(ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y))) lock.withLock {
map.remove(ChunkPos.toLong(geometry.x.chunk(x), geometry.y.chunk(y)))?.remove()
}
} }
override fun iterator(): Iterator<ChunkType> {
return map.values.iterator()
}
override val size: Int
get() = map.size
} }
inner class ArrayChunkMap : ChunkMap() { inner class ArrayChunkMap : ChunkMap() {
private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE)) private val map = Object2DArray.nulls<ChunkType>(divideUp(geometry.size.x, CHUNK_SIZE), divideUp(geometry.size.y, CHUNK_SIZE))
private val existing = ObjectArraySet<ChunkPos>()
private fun getRaw(x: Int, y: Int): ChunkType? { private fun getRaw(x: Int, y: Int): ChunkType? {
return map[x, y] return map[x, y]
@ -154,7 +167,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
override fun compute(x: Int, y: Int): ChunkType? { override fun compute(x: Int, y: Int): ChunkType? {
if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null if (!geometry.x.inBoundsChunk(x) || !geometry.y.inBoundsChunk(y)) return null
return map[x, y] ?: lock.withLock { map[x, y] ?: create(x, y).also { map[x, y] = it } } return map[x, y] ?: lock.withLock { map[x, y] ?: create(x, y).also { existing.add(ChunkPos(x, y)); map[x, y] = it } }
} }
override fun getCell(x: Int, y: Int): AbstractCell { override fun getCell(x: Int, y: Int): AbstractCell {
@ -177,8 +190,36 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
map[geometry.x.chunk(x), geometry.y.chunk(y)] = null lock.withLock {
val x = geometry.x.chunk(x)
val y = geometry.y.chunk(y)
val get = map[x, y]
if (get != null) {
existing.remove(ChunkPos(x, y))
get.remove()
map[x, y] = null
}
}
} }
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 val size: Int
get() = existing.size
} }
val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap() val chunkMap: ChunkMap = if (geometry.size.x <= 32000 && geometry.size.y <= 32000) ArrayChunkMap() else SparseChunkMap()
@ -193,7 +234,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
fun think() { fun think() {
try { try {
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
val entities = lock.withLock { ObjectArrayList(entities) } val entities = ObjectArrayList(entities)
ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), { it.movement.move() })).join() ForkJoinPool.commonPool().submit(ParallelPerform(entities.spliterator(), { it.movement.move() })).join()
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
@ -207,7 +248,12 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
} }
mailbox.executeQueuedTasks() mailbox.executeQueuedTasks()
val objects = lock.withLock { ObjectArrayList(objects) }
lock
.withLock { ObjectArrayList(chunkMap.iterator()) }
.forEach { it.think() }
val objects = ObjectArrayList(objects)
for (ent in objects) { for (ent in objects) {
ent.thinkShared() ent.thinkShared()

View File

@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.util.MailboxExecutorService
import ru.dbotthepony.kstarbound.util.get import ru.dbotthepony.kstarbound.util.get
import ru.dbotthepony.kstarbound.util.set import ru.dbotthepony.kstarbound.util.set
@ -24,17 +25,13 @@ import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.properties.Delegates
open class WorldObject( open class WorldObject(
val world: World<*, *>,
val prototype: Registry.Entry<ObjectDefinition>, val prototype: Registry.Entry<ObjectDefinition>,
val pos: Vector2i, val pos: Vector2i,
) : JsonDriven(prototype.file?.computeDirectory() ?: "/") { ) : JsonDriven(prototype.file?.computeDirectory() ?: "/") {
constructor(world: World<*, *>, data: JsonObject) : this( fun deserialize(data: JsonObject) {
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") }
) {
direction = data.get("direction", directions) { Side.LEFT } direction = data.get("direction", directions) { Side.LEFT }
orientationIndex = data.get("orientationIndex", -1) orientationIndex = data.get("orientationIndex", -1)
interactive = data.get("interactive", false) 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 // internal runtime properties
// //
val clientWorld get() = world as ClientWorld inline val clientWorld get() = world as ClientWorld
val orientations = prototype.value.orientations inline val serverWorld get() = world as ServerWorld
inline val orientations get() = prototype.value.orientations
protected val renderParamLocations = Object2ObjectOpenHashMap<String, () -> String?>() protected val renderParamLocations = Object2ObjectOpenHashMap<String, () -> String?>()
private var frame = 0 private var frame = 0
set(value) { set(value) {
@ -127,18 +127,15 @@ open class WorldObject(
protected open fun innerSpawn() {} protected open fun innerSpawn() {}
protected open fun innerRemove() {} protected open fun innerRemove() {}
fun spawn() { fun spawn(world: World<*, *>) {
if (isSpawned) return check(!isSpawned) { "Already spawned in ${this.world}!" }
this.world = world
isSpawned = true isSpawned = true
innerSpawn()
world.mailbox.execute { invalidate()
world.objects.add(this)
innerSpawn()
invalidate()
}
} }
open fun remove() { fun remove() {
if (isRemoved || !isSpawned) return if (isRemoved || !isSpawned) return
isRemoved = true isRemoved = true