KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt

399 lines
10 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kbox2d.api.BodyDef
import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.api.ILiquidState
import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kstarbound.world.api.OffsetCellAccess
import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.api.TileView
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2d
import java.util.*
import kotlin.collections.ArrayList
/**
* Чанк мира
*
* Хранит в себе тайлы и ентити внутри себя
*
* Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру,
* что будет называться 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(value) {
field = value
promote()
}
var tileChangeset = 0
private set
var liquidChangeset = 0
private set
var cellChangeset = 0
private set
var collisionChangeset = 0
private set
var foregroundChangeset = 0
private set
var backgroundChangeset = 0
private set
private var isEmpty = true
fun promote() {
if (isEmpty) {
isEmpty = false
world.chunkMap.promote(this as This)
}
}
protected val cells = lazy {
Object2DArray.nulls<Cell>(CHUNK_SIZE, CHUNK_SIZE)
}
override fun getCell(x: Int, y: Int): IChunkCell {
var get = cells.value[x, y]
if (get == null) {
get = Cell(x, y)
cells.value[x, y] = get
}
return get
}
override fun getCellDirect(x: Int, y: Int): IChunkCell {
return getCell(x, y)
}
// 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())
var isPhysicsDirty = false
private val collisionCache = ArrayList<AABB>()
private val collisionCacheView = Collections.unmodifiableCollection(collisionCache)
private val body by lazy {
world.physics.createBody(BodyDef(
position = pos.tile.toDoubleVector(),
userData = this
))
}
private val collisionChains = ArrayList<B2Fixture>()
protected open fun foregroundChanges(cell: Cell) {
cellChanges(cell)
tileChangeset++
collisionChangeset++
foregroundChangeset++
markPhysicsDirty()
}
protected open fun backgroundChanges(cell: Cell) {
cellChanges(cell)
tileChangeset++
backgroundChangeset++
}
protected open fun liquidChanges(cell: Cell) {
cellChanges(cell)
liquidChangeset++
}
protected open fun cellChanges(cell: Cell) {
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)
}
fun markPhysicsDirty() {
if (isPhysicsDirty)
return
isPhysicsDirty = true
world.dirtyPhysicsChunks.add(this as This)
}
inner class Cell(x: Int, y: Int) : IChunkCell {
override val x: Int = x + pos.x * CHUNK_SIZE
override val y: Int = y + pos.y * CHUNK_SIZE
inner class Tile(private val foreground: Boolean) : ITileState {
private fun change() {
if (foreground) {
foregroundChanges(this@Cell)
} else {
backgroundChanges(this@Cell)
}
}
override var material: TileDefinition = BuiltinMetaMaterials.NULL
set(value) {
if (field !== value) {
field = value
change()
}
}
override var modifier: MaterialModifier? = null
set(value) {
if (field !== value) {
field = value
change()
}
}
override var color: TileColor = TileColor.DEFAULT
set(value) {
if (field != value) {
field = value
change()
}
}
override var hueShift: Float = 0f
set(value) {
if (field != value) {
field = value.coerceIn(0f, 360f)
change()
}
}
override var modifierHueShift: Float = 0f
set(value) {
if (field != value) {
field = value.coerceIn(0f, 360f)
change()
}
}
}
inner class Liquid : ILiquidState {
override var def: LiquidDefinition? = null
set(value) {
if (field !== value) {
field = value
liquidChanges(this@Cell)
}
}
override var level: Float = 0f
set(value) {
if (field != value) {
field = value
liquidChanges(this@Cell)
}
}
override var pressure: Float = 0f
set(value) {
if (field != value) {
field = value
liquidChanges(this@Cell)
}
}
override var isInfinite: Boolean = false
set(value) {
if (field != value) {
field = value
liquidChanges(this@Cell)
}
}
}
override val foreground = Tile(true)
override val background = Tile(false)
override val liquid = Liquid()
override var dungeonId: Int = 0
set(value) {
if (field != value) {
field = value
cellChanges(this)
}
}
override var biome: Int = 0
set(value) {
if (field != value) {
field = value
cellChanges(this)
}
}
override var envBiome: Int = 0
set(value) {
if (field != value) {
field = value
cellChanges(this)
}
}
override var isIndestructible: Boolean = false
set(value) {
if (field != value) {
field = value
cellChanges(this)
}
}
}
fun bakeCollisions() {
if (collisionChangeset == changeset)
return
collisionChangeset = changeset
collisionCache.clear()
for (box in collisionChains) {
body.destroyFixture(box)
}
collisionChains.clear()
val xyAdd = Vector2d(pos.x * CHUNK_SIZEd, pos.y * CHUNK_SIZEd)
val seen = BooleanArray(CHUNK_SIZE * CHUNK_SIZE)
/*for (y in 0 .. CHUNK_SIZE_FF) {
for (x in 0..CHUNK_SIZE_FF) {
if (!seen[x or (y shl CHUNK_SIZE_BITS)] && cells[x, y].foreground.material != null) {
val depthFirst = RectTileFlooderDepthFirst(this, seen, x, y)
val sizeFirst = RectTileFlooderSizeFirst(this, seen, x, y)
val xSpanDepth = depthFirst.maxs.x - depthFirst.mins.x
val ySpanDepth = depthFirst.maxs.y - depthFirst.mins.y
val xSpanSize = sizeFirst.maxs.x - sizeFirst.mins.x
val ySpanSize = sizeFirst.maxs.y - sizeFirst.mins.y
val aabb: AABB
if (xSpanDepth * ySpanDepth > xSpanSize * ySpanSize) {
depthFirst.markSeen()
aabb = AABB(depthFirst.mins.toDoubleVector(), depthFirst.maxs.toDoubleVector() + Vector2d.POSITIVE_XY)
} else {
sizeFirst.markSeen()
aabb = AABB(sizeFirst.mins.toDoubleVector(), sizeFirst.maxs.toDoubleVector() + Vector2d.POSITIVE_XY)
}
collisionChains.add(body.createFixture(FixtureDef(
shape = PolygonShape().also { it.setAsBox(aabb.width / 2.0, aabb.height / 2.0, aabb.centre, 0.0) },
friction = 0.4,
)))
collisionCache.add(aabb + xyAdd)
}
}
}*/
}
/**
* Возвращает список AABB тайлов этого слоя
*
* Данный список напрямую указывает на внутреннее состояние и будет изменён при перестройке
* коллизии чанка, поэтому если необходим стабильный список, его необходимо скопировать
*/
fun collisionLayers(): Collection<AABB> {
bakeCollisions()
return collisionCacheView
}
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)
}
protected val entities = ReferenceOpenHashSet<Entity>()
protected abstract fun onEntityAdded(entity: Entity)
protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This)
protected abstract fun onEntityTransferedFromThis(entity: Entity, otherChunk: This)
protected abstract fun onEntityRemoved(entity: Entity)
fun addEntity(entity: Entity) {
if (!entities.add(entity)) {
throw IllegalArgumentException("Already having having entity $entity")
}
changeset++
onEntityAdded(entity)
}
fun transferEntity(entity: Entity, otherChunk: Chunk<*, *>) {
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++
onEntityTransferedToThis(entity, otherChunk as This)
otherChunk.onEntityTransferedFromThis(entity, this as This)
if (!otherChunk.entities.remove(entity)) {
throw IllegalStateException("Unable to remove $entity from $otherChunk after transfer")
}
}
fun removeEntity(entity: Entity) {
if (!entities.remove(entity)) {
throw IllegalArgumentException("Already not having entity $entity")
}
changeset++
onEntityRemoved(entity)
}
override fun toString(): String {
return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)"
}
companion object {
private val aabbBase = AABB(
Vector2d.ZERO,
Vector2d(CHUNK_SIZE.toDouble(), CHUNK_SIZE.toDouble()),
)
}
}