package ru.dbotthepony.kstarbound.world import ru.dbotthepony.kstarbound.api.IStruct2d import ru.dbotthepony.kstarbound.api.IStruct2i import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.math.* import java.util.* import kotlin.collections.ArrayList import kotlin.math.absoluteValue /** * Представляет из себя класс, который содержит состояние тайла на заданной позиции */ data class ChunkTile(val chunk: Chunk.TileLayer, val def: TileDefinition) { var color = 0 set(value) { field = value chunk.incChangeset() } var forceVariant = -1 set(value) { field = value chunk.incChangeset() } } interface ITileMap { /** * Относительная проверка находится ли координата вне границ чанка */ fun isOutside(x: Int, y: Int): Boolean { return x !in 0 until CHUNK_SIZE || y !in 0 until CHUNK_SIZE } } /** * Предоставляет интерфейс для доступа к тайлам в чанке */ interface ITileGetter : ITileMap { /** * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ operator fun get(x: Int, y: Int): ChunkTile? /** * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ operator fun get(pos: Vector2i) = get(pos.x, pos.y) /** * Возвращает итератор пар * * Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка */ val posToTile: Iterator> get() { return object : Iterator> { private var x = 0 private var y = 0 private fun idx() = x + CHUNK_SIZE * y override fun hasNext(): Boolean { return idx() < CHUNK_SIZE * CHUNK_SIZE } override fun next(): Pair { if (!hasNext()) { throw IllegalStateException("Already iterated everything!") } val tile = this@ITileGetter[x, y] val pos = Vector2i(x, y) x++ if (x >= CHUNK_SIZE) { y++ x = 0 } return pos to tile } } } } /** * Интерфейс предоставляет из себя описание класса, который имеет координаты чанка */ interface IChunkPositionable : ITileMap { val pos: ChunkPos /** * Возвращает псевдослучайное Long для заданной позиции * * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции */ fun randomLongFor(x: Int, y: Int): Long { var long = (x or (pos.x shl CHUNK_SHIFT)) * 738548L + (y or (pos.y shl CHUNK_SHIFT)) * 2191293543L long = long xor 8339437585692L long = (long ushr 4) or (long shl 52) long *= 7848344324L long = (long ushr 12) or (long shl 44) return long } /** * Возвращает псевдослучайное нормализированное Double для заданной позиции * * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции */ fun randomDoubleFor(x: Int, y: Int): Double { return (randomLongFor(x, y) / 9.223372036854776E18) / 2.0 + 0.5 } /** * Возвращает псевдослучайное Long для заданной позиции * * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции */ fun randomLongFor(pos: Vector2i) = randomLongFor(pos.x, pos.y) /** * Возвращает псевдослучайное нормализированное Double для заданной позиции * * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции */ fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) } /** * Предоставляет интерфейс по установке тайлов в чанке */ interface ITileSetter : ITileMap { /** * Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? /** * Устанавливает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка */ operator fun set(pos: Vector2i, tile: TileDefinition?) = set(pos.x, pos.y, tile) } interface ITileGetterSetter : ITileGetter, ITileSetter interface ITileChunk : ITileGetter, IChunkPositionable interface IMutableTileChunk : ITileChunk, ITileSetter const val CHUNK_SHIFT = 5 const val CHUNK_SIZE = 1 shl CHUNK_SHIFT // 32 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 const val CHUNK_SIZEf = CHUNK_SIZE.toFloat() const val CHUNK_SIZEd = CHUNK_SIZE.toDouble() data class ChunkPos(override val x: Int, override val y: Int) : IVector2i() { constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) override fun make(x: Int, y: Int) = ChunkPos(x, y) val firstBlock get() = Vector2i(x shl CHUNK_SHIFT, y shl CHUNK_SHIFT) val lastBlock get() = Vector2i(((x + 1) shl CHUNK_SHIFT) - 1, ((y + 1) shl CHUNK_SHIFT) - 1) companion object { val ZERO = ChunkPos(0, 0) fun fromTilePosition(input: IStruct2i): ChunkPos { val (x, y) = input return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) } fun fromTilePosition(input: IStruct2d): ChunkPos { val (x, y) = input return fromTilePosition(x, y) } fun fromTilePosition(x: Int, y: Int): ChunkPos { return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) } fun fromTilePosition(x: Double, y: Double): ChunkPos { return ChunkPos(tileToChunkComponent(roundByAbsoluteValue(x)), tileToChunkComponent(roundByAbsoluteValue(y))) } fun normalizeCoordinate(input: Int): Int { val band = input and CHUNK_SIZE_FF if (band < 0) { return band + CHUNK_SIZE_FF } return band } fun tileToChunkComponent(comp: Int): Int { if (comp < 0) { return -(comp.absoluteValue shr CHUNK_SHIFT) - 1 } return comp shr CHUNK_SHIFT } } } /** * Предоставляет доступ к чанку и его соседям * * В основном для использования в местах, где нужен не мир, а определённый чанк мира, * и при этом координаты проверяются относительно чанка и могут спокойно выйти за его пределы, * с желанием получить тайл из соседнего чанка */ open class TileChunkView( open val center: ITileChunk, open val right: ITileChunk?, open val top: ITileChunk?, open val topRight: ITileChunk?, open val topLeft: ITileChunk?, open val left: ITileChunk?, open val bottom: ITileChunk?, open val bottomLeft: ITileChunk?, open val bottomRight: ITileChunk?, ) : ITileChunk { override fun get(x: Int, y: Int): ChunkTile? { if (x in 0 .. CHUNK_SIZE_FF) { if (y in 0 .. CHUNK_SIZE_FF) { return center[x, y] } if (y < 0) { return bottom?.get(x, y + CHUNK_SIZE) } else { return top?.get(x, y - CHUNK_SIZE) } } if (x < 0) { if (y in 0 .. CHUNK_SIZE_FF) { return left?.get(x + CHUNK_SIZE, y) } if (y < 0) { return bottomLeft?.get(x + CHUNK_SIZE, y + CHUNK_SIZE) } else { return topLeft?.get(x + CHUNK_SIZE, y - CHUNK_SIZE) } } else { if (y in 0 .. CHUNK_SIZE_FF) { return right?.get(x - CHUNK_SIZE, y) } if (y < 0) { return bottomRight?.get(x - CHUNK_SIZE, y + CHUNK_SIZE) } else { return topRight?.get(x - CHUNK_SIZE, y - CHUNK_SIZE) } } } override val pos: ChunkPos get() = center.pos } class MutableTileChunkView( override val center: IMutableTileChunk, override val right: IMutableTileChunk?, override val top: IMutableTileChunk?, override val topRight: IMutableTileChunk?, override val topLeft: IMutableTileChunk?, override val left: IMutableTileChunk?, override val bottom: IMutableTileChunk?, override val bottomLeft: IMutableTileChunk?, override val bottomRight: IMutableTileChunk?, ) : TileChunkView(center, right, top, topRight, topLeft, left, bottom, bottomLeft, bottomRight), IMutableTileChunk { override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? { if (x in 0 .. CHUNK_SIZE_FF) { if (y in 0 .. CHUNK_SIZE_FF) { return center.set(x, y, tile) } if (y < 0) { return bottom?.set(x, y + CHUNK_SIZE, tile) } else { return top?.set(x, y - CHUNK_SIZE, tile) } } if (x < 0) { if (y in 0 .. CHUNK_SIZE_FF) { return left?.set(x + CHUNK_SIZE, y, tile) } if (y < 0) { return bottomLeft?.set(x + CHUNK_SIZE, y + CHUNK_SIZE, tile) } else { return topLeft?.set(x + CHUNK_SIZE, y - CHUNK_SIZE, tile) } } else { if (y in 0 .. CHUNK_SIZE_FF) { return right?.set(x - CHUNK_SIZE, y, tile) } if (y < 0) { return bottomRight?.set(x - CHUNK_SIZE, y + CHUNK_SIZE, tile) } else { return topRight?.set(x - CHUNK_SIZE, y - CHUNK_SIZE, tile) } } } } /** * Чанк мира * * Хранит в себе тайлы и ентити внутри себя * * Считается, что один тайл имеет форму квадрата и сторона квадрата примерно равна полуметру, * что будет называться Starbound Unit * * Весь игровой мир будет измеряться в Starbound Unit'ах */ open class Chunk(val world: World<*>?, val pos: ChunkPos) { /** * Возвращает счётчик изменений чанка */ var changeset = 0 private set fun incChangeset() { changeset++ } val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) inner class TileLayer : IMutableTileChunk { /** * Возвращает счётчик изменений этого слоя */ var changeset = 0 private set fun incChangeset() { changeset++ this@Chunk.changeset++ } private val collisionCache = ArrayList() private val collisionCacheView = Collections.unmodifiableCollection(collisionCache) private var collisionChangeset = -1 // максимально грубое комбинирование тайлов в бруски для коллизии // TODO: https://ru.wikipedia.org/wiki/R-дерево_(структура_данных) private fun bakeCollisions() { collisionChangeset = changeset val seen = BooleanArray(tiles.size) collisionCache.clear() val xAdd = pos.x * CHUNK_SIZEd val yAdd = pos.y * CHUNK_SIZEd for (y in 0 .. CHUNK_SIZE_FF) { var first: Int? = null var last = 0 for (x in 0 .. CHUNK_SIZE_FF) { if (tiles[x or (y shl CHUNK_SHIFT)] != null) { if (first == null) { first = x } last = x } else { if (first != null) { collisionCache.add(AABB( Vector2d(x = xAdd + first.toDouble(), y = y.toDouble() + yAdd), Vector2d(x = xAdd + last.toDouble() + 1.0, y = y.toDouble() + 1.0 + yAdd), )) first = null } } } if (first != null) { collisionCache.add(AABB( Vector2d(x = first.toDouble() + xAdd, y = y.toDouble() + yAdd), Vector2d(x = last.toDouble() + 1.0 + xAdd, y = y.toDouble() + 1.0 + yAdd), )) } } } /** * Возвращает список AABB тайлов этого слоя * * Данный список напрямую указывает на внутреннее состояние и будет изменён при перестройке * коллизии чанка, поэтому если необходим стабильный список, его необходимо скопировать */ fun collisionLayers(): Collection { if (collisionChangeset != changeset) { bakeCollisions() } return collisionCacheView } override val pos: ChunkPos get() = this@Chunk.pos /** * Хранит тайлы как x + y * CHUNK_SIZE */ private val tiles = arrayOfNulls(CHUNK_SIZE * CHUNK_SIZE) override operator fun get(x: Int, y: Int): ChunkTile? { if (isOutside(x, y)) return null return tiles[x or (y shl CHUNK_SHIFT)] } operator fun set(x: Int, y: Int, tile: ChunkTile?) { if (isOutside(x, y)) throw IndexOutOfBoundsException("Trying to set tile ${tile?.def?.materialName} at $x $y, but that is outside of chunk's range") changeset++ tiles[x or (y shl CHUNK_SHIFT)] = tile } override operator fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? { if (isOutside(x, y)) throw IndexOutOfBoundsException("Trying to set tile ${tile?.materialName} at $x $y, but that is outside of chunk's range") val chunkTile = if (tile != null) ChunkTile(this, tile) else null this[x, y] = chunkTile changeset++ return chunkTile } override fun randomLongFor(x: Int, y: Int): Long { return super.randomLongFor(x, y) xor (world?.seed ?: 0L) } } val foreground = TileLayer() val background = TileLayer() companion object { val EMPTY = object : IMutableTileChunk { override val pos = ChunkPos(0, 0) override fun get(x: Int, y: Int): ChunkTile? = null override fun set(x: Int, y: Int, tile: TileDefinition?): ChunkTile? = null } private val aabbBase = AABB( Vector2d.ZERO, Vector2d(CHUNK_SIZE.toDouble(), CHUNK_SIZE.toDouble()), ) } }