264 lines
7.0 KiB
Kotlin
264 lines
7.0 KiB
Kotlin
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<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
|
||
|
||
val entities = ReferenceOpenHashSet<Entity>()
|
||
val objects = ReferenceOpenHashSet<WorldObject>()
|
||
protected val subscribers = ObjectArraySet<IChunkListener>()
|
||
|
||
// 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<out AbstractCell>) {
|
||
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()),
|
||
)
|
||
}
|
||
}
|