273 lines
8.0 KiB
Kotlin
273 lines
8.0 KiB
Kotlin
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<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(
|
||
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<Vector2i, TileHealth.Tile>()
|
||
protected val foregroundHealth = HashMap<Vector2i, TileHealth.Tile>()
|
||
protected val collisionCacheDirty = Boolean2DArray.allocate(width, height)
|
||
protected val collisionCache by lazy {
|
||
Object2DArray(width, height) { _, _ -> ObjectArrayList<CollisionPoly>(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<Vector2i>(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<CollisionPoly>) {
|
||
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<out AbstractCell>) {
|
||
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<ImmutableCell> {
|
||
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) {
|
||
|
||
}
|
||
}
|