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, This : Chunk>( 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(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() 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() 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 { 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() 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()), ) } }