package ru.dbotthepony.kstarbound.world import it.unimi.dsi.fastutil.objects.ObjectArrayList import ru.dbotthepony.kommons.arrays.Boolean2DArray import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.server.world.ServerChunk 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.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import java.util.BitSet import java.util.concurrent.CopyOnWriteArraySet import kotlin.math.max import kotlin.math.min /** * Чанк мира * * Хранит в себе тайлы и ентити внутри себя * * Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру, * что будет называться 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 abstract val state: ChunkState 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) 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 = AABBi(pos.tile, pos.tile + Vector2i(width, height)) val aabbd = aabb.toDoubleAABB() protected val cells = lazy { Object2DArray(width, height, AbstractCell.NULL) } protected val backgroundHealth = HashMap() protected val foregroundHealth = HashMap() protected val collisionCacheDirty = Boolean2DArray.allocate(width, height) protected val collisionCache by lazy { Object2DArray(width, height) { _, _ -> ObjectArrayList(0) } } init { for (x in 0 until width) for (y in 0 until height) collisionCacheDirty[x, y] = true } private var hasDirtyCollisions = false // bulk mark collision dirty of neighbour chunks as well as ours protected fun signalChunkContentsUpdated() { val signalPositions = ObjectArrayList(24) for (x in 1 .. 2) { for (y in 1 .. 2) { signalPositions.add(pos.tile + Vector2i(width + x, height + y)) signalPositions.add(pos.tile + Vector2i(width, height + y)) signalPositions.add(pos.tile + Vector2i(width + x, height)) signalPositions.add(pos.tile + Vector2i(-x, -y)) signalPositions.add(pos.tile + Vector2i(0, -y)) signalPositions.add(pos.tile + Vector2i(-x, 0)) } } for (pos in signalPositions) { val actualCellPosition = world.geometry.wrap(pos) val chunk = world.chunkMap[world.geometry.chunkFromCell(actualCellPosition)] ?: continue chunk.hasDirtyCollisions = true chunk.collisionCacheDirty[actualCellPosition.x - chunk.pos.tileX, actualCellPosition.y - chunk.pos.tileY] = true } hasDirtyCollisions = true backgroundHealth.clear() foregroundHealth.clear() for (x in 0 until width) { for (y in 0 until height) { collisionCacheDirty[x, y] = true } } } private val collisionsLock = Any() fun getCollisions(x: Int, y: Int, target: MutableCollection) { if (hasDirtyCollisions) { synchronized(collisionsLock) { if (hasDirtyCollisions) { var minX = width var minY = height var maxX = 0 var maxY = 0 for (x in 0 until width) { for (y in 0 until height) { if (collisionCacheDirty[x, y]) { minX = min(minX, x) minY = min(minY, y) maxX = max(maxX, x) maxY = max(maxY, y) } } } for (x in minX .. maxX) { for (y in minY .. maxY) { if (collisionCacheDirty[x, y]) { collisionCache[x, y].clear() getBlocksMarchingSquares(pos.tileX + x, pos.tileY + y, world.foreground, CollisionType.DYNAMIC, collisionCache[x, y]) getBlockPlatforms(pos.tileX + x, pos.tileY + y, world.foreground, CollisionType.PLATFORM, collisionCache[x, y]) collisionCacheDirty[x, y] = false } } } } } hasDirtyCollisions = false } target.addAll(collisionCache[x, y]) } fun loadCells(source: Object2DArray) { val ours = cells.value for (x in 0 until width) { for (y in 0 until height) { ours[x, y] = source[x, y].immutable() } } signalChunkContentsUpdated() } fun copyCells(): Object2DArray { if (cells.isInitialized()) { val cells = cells.value return Object2DArray(width, height) { x, y -> cells[x, y] } } else { return Object2DArray(width, height, AbstractCell.NULL) } } override fun getCell(x: Int, y: Int): AbstractCell { return if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL } final override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { val old = if (cells.isInitialized()) cells.value[x, y] else AbstractCell.NULL val new = cell.immutable() if (old != new) { cells.value[x, y] = new hasDirtyCollisions = true collisionCacheDirty[x, y] = true for (xoff in -2 .. 2) { for (yoff in -2 .. 2) { val actualCellPosition = world.geometry.wrap(pos.tile + Vector2i(x + xoff, y + yoff)) val chunk = world.chunkMap[world.geometry.chunkFromCell(actualCellPosition)] ?: continue chunk.hasDirtyCollisions = true chunk.collisionCacheDirty[actualCellPosition.x - chunk.pos.tileX, actualCellPosition.y - chunk.pos.tileY] = true } } if (old.foreground != new.foreground) { foregroundHealth.remove(Vector2i(x, y)) foregroundChanges(x, y, new) } if (old.background != new.background) { backgroundHealth.remove(Vector2i(x, y)) backgroundChanges(x, y, new) } if (old.liquid != new.liquid) { liquidChanges(x, y, new) } cellChanges(x, y, new) } return true } protected open fun foregroundChanges(x: Int, y: Int, cell: ImmutableCell) { tileChangeset++ foregroundChangeset++ } protected open fun backgroundChanges(x: Int, y: Int, cell: ImmutableCell) { tileChangeset++ backgroundChangeset++ } protected open fun liquidChanges(x: Int, y: Int, cell: ImmutableCell) { liquidChangeset++ } protected open fun cellChanges(x: Int, y: Int, cell: ImmutableCell) { changeset++ cellChangeset++ } 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 toString(): String { return "${this::class.simpleName}(pos=$pos, world=$world)" } open fun remove() { } open fun tick(delta: Double) { } }