399 lines
10 KiB
Kotlin
399 lines
10 KiB
Kotlin
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()),
|
||
)
|
||
}
|
||
}
|