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 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 import kotlin.concurrent.withLock /** * Чанк мира * * Хранит в себе тайлы и ентити внутри себя * * Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру, * что будет называться Starbound Unit * * Весь игровой мир будет измеряться в Starbound Unit'ах */ abstract class Chunk, This : Chunk>( val world: WorldType, val pos: ChunkPos, ) : ICellAccess { var changeset = 0 private set var tileChangeset = 0 private set var liquidChangeset = 0 private set var cellChangeset = 0 private set var foregroundChangeset = 0 private set var backgroundChangeset = 0 private set val entities = ReferenceOpenHashSet() 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) for (x in 0 until CHUNK_SIZE) { for (y in 0 until CHUNK_SIZE) { ours[x, y] = source[x, y].immutable() } } } override fun getCell(x: Int, y: Int): AbstractCell { if (!cells.isInitialized()) return AbstractCell.NULL return cells.value[x, y] } override fun getCellDirect(x: Int, y: Int): AbstractCell { return getCell(x, y) } 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 new = cell.immutable() if (old != new) { cells.value[ix, iy] = new if (old.foreground != new.foreground) { foregroundChanges(ix, iy, new) } if (old.background != new.background) { backgroundChanges(ix, iy, new) } if (old.liquid != new.liquid) { liquidChanges(ix, iy, new) } cellChanges(ix, iy, new) } return true } protected open fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) { cellChanges(x, y, cell) tileChangeset++ foregroundChangeset++ } protected open fun backgroundChanges(x: Int, y: Int, cell: ImmutableCell) { cellChanges(x, y, cell) tileChangeset++ backgroundChangeset++ } protected open fun liquidChanges(x: Int, y: Int, cell: ImmutableCell) { cellChanges(x, y, cell) liquidChangeset++ } protected open fun cellChanges(x: Int, y: Int, cell: ImmutableCell) { changeset++ cellChangeset++ subscribers.forEach { it.cellChanges(x, y, cell) } } protected inline fun forEachNeighbour(block: (This) -> Unit) { world.chunkMap[pos.left]?.let(block) world.chunkMap[pos.right]?.let(block) world.chunkMap[pos.top]?.let(block) world.chunkMap[pos.bottom]?.let(block) world.chunkMap[pos.topLeft]?.let(block) world.chunkMap[pos.topRight]?.let(block) world.chunkMap[pos.bottomLeft]?.let(block) world.chunkMap[pos.bottomRight]?.let(block) } override fun randomLongFor(x: Int, y: Int): Long { return world.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS) } fun addListener(subscriber: IChunkListener) { subscribers.add(subscriber) } fun removeListener(subscriber: IChunkListener) { subscribers.remove(subscriber) } fun addEntity(entity: Entity) { world.lock.withLock { if (!entities.add(entity)) { throw IllegalArgumentException("Already having having entity $entity") } changeset++ subscribers.forEach { it.onEntityAdded(entity) } } } fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) { world.lock.withLock { if (otherChunk == this) throw IllegalArgumentException("what?") if (this::class.java != otherChunk::class.java) { throw IllegalArgumentException("Incompatible types: $this !is $otherChunk") } if (!entities.add(entity)) { throw IllegalArgumentException("Already containing $entity") } changeset++ otherChunk.subscribers.forEach { it.onEntityRemoved(entity) } subscribers.forEach { it.onEntityAdded(entity) } if (!otherChunk.entities.remove(entity)) { throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer") } } } fun removeEntity(entity: Entity) { world.lock.withLock { if (!entities.remove(entity)) { throw IllegalArgumentException("Already not having entity $entity") } changeset++ subscribers.forEach { it.onEntityRemoved(entity) } } } override fun toString(): String { return "${this::class.simpleName}(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 { private val aabbBase = AABB( Vector2d.ZERO, Vector2d(CHUNK_SIZE.toDouble(), CHUNK_SIZE.toDouble()), ) } }