Move classes around

This commit is contained in:
DBotThePony 2022-07-29 14:06:03 +07:00
parent af97c80cdd
commit aa9d379d41
Signed by: DBot
GPG Key ID: DCC23B5715498507
12 changed files with 892 additions and 852 deletions

View File

@ -6,34 +6,11 @@ import ru.dbotthepony.kstarbound.client.gl.VertexTransformers
import ru.dbotthepony.kstarbound.client.render.renderLayeredList
import ru.dbotthepony.kstarbound.defs.ParallaxPrototype
import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB
import ru.dbotthepony.kstarbound.util.DoubleEdgeProgression
import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.util2d.AABB
class DoubleEdgeProgression : Iterator<Int> {
var value = 0
override fun hasNext(): Boolean {
return true
}
override fun next(): Int {
return nextInt()
}
fun nextInt(): Int {
return if (value > 0) {
val ret = value
value = -value
ret
} else {
val ret = value
value = -value + 1
ret
}
}
}
class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWorld, ClientChunk>(seed) {
init {
physics.debugDraw = client.gl.box2dRenderer

View File

@ -0,0 +1,25 @@
package ru.dbotthepony.kstarbound.util
class DoubleEdgeProgression : Iterator<Int> {
var value = 0
override fun hasNext(): Boolean {
return true
}
override fun next(): Int {
return nextInt()
}
fun nextInt(): Int {
return if (value > 0) {
val ret = value
value = -value
ret
} else {
val ret = value
value = -value + 1
ret
}
}
}

View File

@ -0,0 +1,35 @@
package ru.dbotthepony.kstarbound.util
class Timer(val period: Double, val executionTimes: Int, val func: (Timer) -> Unit) {
private var counter = 0.0
var cycles = 0
private set
var destroyed: Boolean = false
private set
fun think(delta: Double) {
if (destroyed) {
throw IllegalStateException("This timer is destroyed")
}
counter += delta
if (counter >= period) {
counter -= period
func.invoke(this)
cycles++
}
if (!destroyed && executionTimes > 0 && cycles >= executionTimes) {
destroy()
}
}
fun destroy() {
if (destroyed) {
throw IllegalStateException("Already destroyed")
}
destroyed = true
}
}

View File

@ -5,17 +5,14 @@ import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderDepthFirst
import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderSizeFirst
import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nint.Vector2i
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.math.absoluteValue
/**
* Представляет из себя класс, который содержит состояние тайла на заданной позиции
@ -34,732 +31,6 @@ data class ChunkTile(val chunk: Chunk<*, *>.TileLayer, val def: TileDefinition)
}
}
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)
/**
* Возвращает итератор пар <Vector2i, Тайл?>
*
* Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка
*/
val posToTile: Iterator<Pair<Vector2i, ChunkTile?>> get() {
return object : Iterator<Pair<Vector2i, ChunkTile?>> {
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<Vector2i, ChunkTile?> {
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()
/**
* Сетка чанков идёт как и сетка тайлов.
*
* * Вправо у нас положительный X
* * Влево у нас отрицательный X
* * Вверх у нас положительный Y
* * Вниз у нас отрицательный Y
*/
class ChunkPos(val x: Int, val y: Int) : Comparable<ChunkPos> {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
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)
val top: ChunkPos get() {
return ChunkPos(x, y + 1)
}
val bottom: ChunkPos get() {
return ChunkPos(x, y - 1)
}
val left: ChunkPos get() {
return ChunkPos(x - 1, y)
}
val topLeft: ChunkPos get() {
return ChunkPos(x - 1, y + 1)
}
val topRight: ChunkPos get() {
return ChunkPos(x + 1, y + 1)
}
val bottomLeft: ChunkPos get() {
return ChunkPos(x - 1, y - 1)
}
val bottomRight: ChunkPos get() {
return ChunkPos(x + 1, y - 1)
}
val right: ChunkPos get() {
return ChunkPos(x + 1, y)
}
override fun equals(other: Any?): Boolean {
if (other is ChunkPos)
return other.x == x && other.y == y
return false
}
override fun hashCode(): Int {
return (y shl 16) xor x
}
override fun toString(): String {
return "ChunkPos[$x $y]"
}
override fun compareTo(other: ChunkPos): Int {
if (x > other.x) {
return 1
} else if (x < other.x) {
return -1
}
return y.compareTo(other.y)
}
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 TileView(
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 MutableTileView(
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?,
) : TileView(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)
}
}
}
}
private data class TileExposure(
var pos: Vector2d,
val left: Boolean,
val right: Boolean,
val up: Boolean,
val down: Boolean,
)
private class TileFlooder(
private val tiles: Array<ChunkTile?>,
private val seen: BooleanArray,
rootx: Int,
rooty: Int
) {
/**
* Tile positions which have at least one face free of neighbours
*/
val exposed = ArrayList<TileExposure>()
private fun get(x: Int, y: Int): Boolean {
if (x < 0 || x > CHUNK_SIZE_FF) {
return false
}
if (y < 0 || y > CHUNK_SIZE_FF) {
return false
}
return tiles[x or (y shl CHUNK_SHIFT)] != null
}
private fun visit(x: Int, y: Int) {
if (seen[x or (y shl CHUNK_SHIFT)])
return
seen[x or (y shl CHUNK_SHIFT)] = true
val left = get(x - 1, y)
val right = get(x + 1, y)
val up = get(x, y + 1)
val down = get(x, y - 1)
if (!left || !right || !up || !down) {
exposed.add(TileExposure(Vector2d(x.toDouble() + 0.5, y.toDouble() + 0.5), !left, !right, !up, !down))
}
if (left) {
visit(x - 1, y)
}
if (right) {
visit(x + 1, y)
}
if (up) {
visit(x, y + 1)
}
if (down) {
visit(x, y - 1)
}
}
init {
visit(rootx, rooty)
}
}
private class RectTileFlooderDepthFirst(
private val tiles: Array<ChunkTile?>,
private val seen: BooleanArray,
rootx: Int,
rooty: Int
) {
val mins: Vector2i
val maxs: Vector2i
private fun filled(x: Int, y: Int): Boolean {
if (x < 0 || x > CHUNK_SIZE_FF) {
return false
}
if (y < 0 || y > CHUNK_SIZE_FF) {
return false
}
return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x or (y shl CHUNK_SHIFT)] != null
}
init {
// expand wide
var widthPositive = 1
while (true) {
if (!filled(rootx + widthPositive, rooty)) {
break
}
widthPositive++
}
var widthNegative = 1
while (true) {
if (!filled(rootx - widthNegative, rooty)) {
break
}
widthNegative++
}
// expand tall
var heightPositive = 1
while (true) {
if (!filled(rootx, rooty + heightPositive)) {
break
}
heightPositive++
}
var heightNegative = 1
while (true) {
if (!filled(rootx, rooty - heightNegative)) {
break
}
heightNegative++
}
widthPositive -= 1
widthNegative -= 1
heightNegative -= 1
heightPositive -= 1
if (heightPositive + heightNegative > widthPositive + widthNegative) {
// height is bigger
// try to expand wide
widthPositive = 0
widthNegative = 0
while (true) {
var escape = false
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx + widthPositive, rooty + i)) {
escape = true
break
}
}
if (escape) {
break
}
widthPositive++
}
while (true) {
var escape = false
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx - widthNegative, rooty + i)) {
escape = true
break
}
}
if (escape) {
break
}
widthNegative++
}
widthNegative -= 1
widthPositive -= 1
} else {
// height is equal or lesser than width
// expand high
heightNegative = 0
heightPositive = 0
while (true) {
var escape = false
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty + heightPositive)) {
escape = true
break
}
}
if (escape) {
break
}
heightPositive++
}
while (true) {
var escape = false
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty - heightNegative)) {
escape = true
break
}
}
if (escape) {
break
}
heightNegative++
}
heightNegative -= 1
heightPositive -= 1
}
mins = Vector2i(rootx - widthNegative, rooty - heightNegative)
maxs = Vector2i(rootx + widthPositive, rooty + heightPositive)
}
fun markSeen() {
for (x in mins.x .. maxs.x) {
for (y in mins.y .. maxs.y) {
seen[x or (y shl CHUNK_SHIFT)] = true
}
}
}
}
private class RectTileFlooderSizeFirst(
private val tiles: Array<ChunkTile?>,
private val seen: BooleanArray,
private val rootx: Int,
private val rooty: Int
) {
val mins: Vector2i
val maxs: Vector2i
private fun filled(x: Int, y: Int): Boolean {
if (x < 0 || x > CHUNK_SIZE_FF) {
return false
}
if (y < 0 || y > CHUNK_SIZE_FF) {
return false
}
return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x or (y shl CHUNK_SHIFT)] != null
}
private var widthPositive = 0
private var widthNegative = 0
private var heightPositive = 0
private var heightNegative = 0
private fun checkLeft(): Boolean {
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx - widthNegative, rooty + i)) {
return false
}
}
return true
}
private fun checkRight(): Boolean {
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx + widthPositive, rooty + i)) {
return false
}
}
return true
}
private fun checkUp(): Boolean {
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty + heightPositive)) {
return false
}
}
return true
}
private fun checkDown(): Boolean {
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty - heightNegative)) {
return false
}
}
return true
}
init {
var expanded = true
var hitLeft = false
var hitRight = false
var hitUp = false
var hitDown = false
while (expanded) {
expanded = false
// expand left
if (!hitLeft) {
widthNegative++
if (!checkLeft()) {
widthNegative--
hitLeft = true
} else {
expanded = true
}
}
// expand up
if (!hitUp) {
heightPositive++
if (!checkUp()) {
heightPositive--
hitUp = true
} else {
expanded = true
}
}
// expand right
if (!hitRight) {
widthPositive++
if (!checkRight()) {
widthPositive--
hitRight = true
} else {
expanded = true
}
}
// expand down
if (!hitDown) {
heightNegative++
if (!checkDown()) {
heightNegative--
hitDown = true
} else {
expanded = true
}
}
}
mins = Vector2i(rootx - widthNegative, rooty - heightNegative)
maxs = Vector2i(rootx + widthPositive, rooty + heightPositive)
}
fun markSeen() {
for (x in mins.x .. maxs.x) {
for (y in mins.y .. maxs.y) {
seen[x or (y shl CHUNK_SHIFT)] = true
}
}
}
}
private fun ccwSortScore(point: Vector2d, axis: Vector2d): Double {
if (point.x > 0.0) {
return point.dot(axis)
@ -1105,9 +376,3 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
)
}
}
private fun <E> AbstractList<E>.addU(e: E) {
if (indexOf(e) == -1) {
add(e)
}
}

View File

@ -0,0 +1,133 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kvector.vector.nint.Vector2i
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()
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)
/**
* Возвращает итератор пар <Vector2i, Тайл?>
*
* Вектор имеет ОТНОСИТЕЛЬНЫЕ значения внутри самого чанка
*/
val posToTile: Iterator<Pair<Vector2i, ChunkTile?>> get() {
return object : Iterator<Pair<Vector2i, ChunkTile?>> {
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<Vector2i, ChunkTile?> {
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

View File

@ -0,0 +1,130 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.math.roundByAbsoluteValue
import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.nint.Vector2i
import kotlin.math.absoluteValue
/**
* Сетка чанков идёт как и сетка тайлов.
*
* * Вправо у нас положительный X
* * Влево у нас отрицательный X
* * Вверх у нас положительный Y
* * Вниз у нас отрицательный Y
*/
class ChunkPos(val x: Int, val y: Int) : Comparable<ChunkPos> {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
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)
val top: ChunkPos
get() {
return ChunkPos(x, y + 1)
}
val bottom: ChunkPos
get() {
return ChunkPos(x, y - 1)
}
val left: ChunkPos
get() {
return ChunkPos(x - 1, y)
}
val topLeft: ChunkPos
get() {
return ChunkPos(x - 1, y + 1)
}
val topRight: ChunkPos
get() {
return ChunkPos(x + 1, y + 1)
}
val bottomLeft: ChunkPos
get() {
return ChunkPos(x - 1, y - 1)
}
val bottomRight: ChunkPos
get() {
return ChunkPos(x + 1, y - 1)
}
val right: ChunkPos
get() {
return ChunkPos(x + 1, y)
}
override fun equals(other: Any?): Boolean {
if (other is ChunkPos)
return other.x == x && other.y == y
return false
}
override fun hashCode(): Int {
return (y shl 16) xor x
}
override fun toString(): String {
return "ChunkPos[$x $y]"
}
override fun compareTo(other: ChunkPos): Int {
if (x > other.x) {
return 1
} else if (x < other.x) {
return -1
}
return y.compareTo(other.y)
}
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
}
}
}

View File

@ -0,0 +1,113 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.defs.TileDefinition
/**
* Предоставляет доступ к чанку и его соседям
*
* В основном для использования в местах, где нужен не мир, а определённый чанк мира,
* и при этом координаты проверяются относительно чанка и могут спокойно выйти за его пределы,
* с желанием получить тайл из соседнего чанка
*/
open class TileView(
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 MutableTileView(
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?,
) : TileView(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)
}
}
}
}

View File

@ -0,0 +1,59 @@
package ru.dbotthepony.kstarbound.world
/**
* Кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков)
*/
interface IWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>> {
val world: WorldType
val chunk: ChunkType
val top: IWorldChunkTuple<WorldType, ChunkType>?
val left: IWorldChunkTuple<WorldType, ChunkType>?
val right: IWorldChunkTuple<WorldType, ChunkType>?
val bottom: IWorldChunkTuple<WorldType, ChunkType>?
val topLeft: IWorldChunkTuple<WorldType, ChunkType>?
val topRight: IWorldChunkTuple<WorldType, ChunkType>?
val bottomLeft: IWorldChunkTuple<WorldType, ChunkType>?
val bottomRight: IWorldChunkTuple<WorldType, ChunkType>?
}
class ProxiedWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>>(
private val parent: IWorldChunkTuple<WorldType, ChunkType>
) : IWorldChunkTuple<WorldType, ChunkType> {
override val world get() = parent.world
override val chunk get() = parent.chunk
override val top: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.top?.let(::ProxiedWorldChunkTuple)
override val left: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.left?.let(::ProxiedWorldChunkTuple)
override val right: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.right?.let(::ProxiedWorldChunkTuple)
override val bottom: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.bottom?.let(::ProxiedWorldChunkTuple)
override val topLeft: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.topLeft?.let(::ProxiedWorldChunkTuple)
override val topRight: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.topRight?.let(::ProxiedWorldChunkTuple)
override val bottomLeft: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.bottomLeft?.let(::ProxiedWorldChunkTuple)
override val bottomRight: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.bottomRight?.let(::ProxiedWorldChunkTuple)
}
class InstantWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>>(
override val world: WorldType,
override val chunk: ChunkType
) : IWorldChunkTuple<WorldType, ChunkType> {
private val _top = world[chunk.top]
private val _left = world[chunk.left]
private val _right = world[chunk.right]
private val _bottom = world[chunk.bottom]
private val _topLeft = world[chunk.topLeft]
private val _topRight = world[chunk.topRight]
private val _bottomLeft = world[chunk.bottomLeft]
private val _bottomRight = world[chunk.bottomRight]
override val top: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _top?.let { InstantWorldChunkTuple(world, it) } }
override val left: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _left?.let { InstantWorldChunkTuple(world, it) } }
override val right: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _right?.let { InstantWorldChunkTuple(world, it) } }
override val bottom: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _bottom?.let { InstantWorldChunkTuple(world, it) } }
override val topLeft: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _topLeft?.let { InstantWorldChunkTuple(world, it) } }
override val topRight: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _topRight?.let { InstantWorldChunkTuple(world, it) } }
override val bottomLeft: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _bottomLeft?.let { InstantWorldChunkTuple(world, it) } }
override val bottomRight: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _bottomRight?.let { InstantWorldChunkTuple(world, it) } }
}

View File

@ -12,6 +12,7 @@ import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.defs.TileDefinition
import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.Timer
import ru.dbotthepony.kstarbound.world.entities.CollisionResolution
import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kstarbound.world.entities.projectile.AbstractProjectileMovementController
@ -21,63 +22,6 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nint.Vector2i
import kotlin.math.absoluteValue
/**
* Кортеж чанка, который содержит родителя (мир) и соседей (кортежи чанков)
*/
interface IWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>> {
val world: WorldType
val chunk: ChunkType
val top: IWorldChunkTuple<WorldType, ChunkType>?
val left: IWorldChunkTuple<WorldType, ChunkType>?
val right: IWorldChunkTuple<WorldType, ChunkType>?
val bottom: IWorldChunkTuple<WorldType, ChunkType>?
val topLeft: IWorldChunkTuple<WorldType, ChunkType>?
val topRight: IWorldChunkTuple<WorldType, ChunkType>?
val bottomLeft: IWorldChunkTuple<WorldType, ChunkType>?
val bottomRight: IWorldChunkTuple<WorldType, ChunkType>?
}
class ProxiedWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>>(
private val parent: IWorldChunkTuple<WorldType, ChunkType>
) : IWorldChunkTuple<WorldType, ChunkType> {
override val world get() = parent.world
override val chunk get() = parent.chunk
override val top: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.top?.let(::ProxiedWorldChunkTuple)
override val left: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.left?.let(::ProxiedWorldChunkTuple)
override val right: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.right?.let(::ProxiedWorldChunkTuple)
override val bottom: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.bottom?.let(::ProxiedWorldChunkTuple)
override val topLeft: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.topLeft?.let(::ProxiedWorldChunkTuple)
override val topRight: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.topRight?.let(::ProxiedWorldChunkTuple)
override val bottomLeft: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.bottomLeft?.let(::ProxiedWorldChunkTuple)
override val bottomRight: IWorldChunkTuple<WorldType, ChunkType>? get() = parent.bottomRight?.let(::ProxiedWorldChunkTuple)
}
class InstantWorldChunkTuple<WorldType : World<WorldType, ChunkType>, ChunkType : Chunk<WorldType, ChunkType>>(
override val world: WorldType,
override val chunk: ChunkType
) : IWorldChunkTuple<WorldType, ChunkType> {
private val _top = world[chunk.top]
private val _left = world[chunk.left]
private val _right = world[chunk.right]
private val _bottom = world[chunk.bottom]
private val _topLeft = world[chunk.topLeft]
private val _topRight = world[chunk.topRight]
private val _bottomLeft = world[chunk.bottomLeft]
private val _bottomRight = world[chunk.bottomRight]
override val top: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _top?.let { InstantWorldChunkTuple(world, it) } }
override val left: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _left?.let { InstantWorldChunkTuple(world, it) } }
override val right: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _right?.let { InstantWorldChunkTuple(world, it) } }
override val bottom: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _bottom?.let { InstantWorldChunkTuple(world, it) } }
override val topLeft: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _topLeft?.let { InstantWorldChunkTuple(world, it) } }
override val topRight: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _topRight?.let { InstantWorldChunkTuple(world, it) } }
override val bottomLeft: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _bottomLeft?.let { InstantWorldChunkTuple(world, it) } }
override val bottomRight: IWorldChunkTuple<WorldType, ChunkType>? by lazy { _bottomRight?.let { InstantWorldChunkTuple(world, it) } }
}
const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
data class WorldSweepResult(
@ -89,40 +33,6 @@ data class WorldSweepResult(
private const val EPSILON = 0.00001
class Timer(val period: Double, val executionTimes: Int, val func: (Timer) -> Unit) {
private var counter = 0.0
var cycles = 0
private set
var destroyed: Boolean = false
private set
fun think(delta: Double) {
if (destroyed) {
throw IllegalStateException("This timer is destroyed")
}
counter += delta
if (counter >= period) {
counter -= period
func.invoke(this)
cycles++
}
if (!destroyed && executionTimes > 0 && cycles >= executionTimes) {
destroy()
}
}
fun destroy() {
if (destroyed) {
throw IllegalStateException("Already destroyed")
}
destroyed = true
}
}
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(val seed: Long = 0L) {
protected val chunkMap = Object2ObjectAVLTreeMap<ChunkPos, ChunkType> cmp@{ a, b ->
return@cmp a.compareTo(b)

View File

@ -0,0 +1,175 @@
package ru.dbotthepony.kstarbound.world.phys
import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.ChunkTile
import ru.dbotthepony.kvector.vector.nint.Vector2i
class RectTileFlooderDepthFirst(
private val tiles: Array<ChunkTile?>,
private val seen: BooleanArray,
rootx: Int,
rooty: Int
) {
val mins: Vector2i
val maxs: Vector2i
private fun filled(x: Int, y: Int): Boolean {
if (x < 0 || x > CHUNK_SIZE_FF) {
return false
}
if (y < 0 || y > CHUNK_SIZE_FF) {
return false
}
return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x or (y shl CHUNK_SHIFT)] != null
}
init {
// expand wide
var widthPositive = 1
while (true) {
if (!filled(rootx + widthPositive, rooty)) {
break
}
widthPositive++
}
var widthNegative = 1
while (true) {
if (!filled(rootx - widthNegative, rooty)) {
break
}
widthNegative++
}
// expand tall
var heightPositive = 1
while (true) {
if (!filled(rootx, rooty + heightPositive)) {
break
}
heightPositive++
}
var heightNegative = 1
while (true) {
if (!filled(rootx, rooty - heightNegative)) {
break
}
heightNegative++
}
widthPositive -= 1
widthNegative -= 1
heightNegative -= 1
heightPositive -= 1
if (heightPositive + heightNegative > widthPositive + widthNegative) {
// height is bigger
// try to expand wide
widthPositive = 0
widthNegative = 0
while (true) {
var escape = false
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx + widthPositive, rooty + i)) {
escape = true
break
}
}
if (escape) {
break
}
widthPositive++
}
while (true) {
var escape = false
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx - widthNegative, rooty + i)) {
escape = true
break
}
}
if (escape) {
break
}
widthNegative++
}
widthNegative -= 1
widthPositive -= 1
} else {
// height is equal or lesser than width
// expand high
heightNegative = 0
heightPositive = 0
while (true) {
var escape = false
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty + heightPositive)) {
escape = true
break
}
}
if (escape) {
break
}
heightPositive++
}
while (true) {
var escape = false
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty - heightNegative)) {
escape = true
break
}
}
if (escape) {
break
}
heightNegative++
}
heightNegative -= 1
heightPositive -= 1
}
mins = Vector2i(rootx - widthNegative, rooty - heightNegative)
maxs = Vector2i(rootx + widthPositive, rooty + heightPositive)
}
fun markSeen() {
for (x in mins.x .. maxs.x) {
for (y in mins.y .. maxs.y) {
seen[x or (y shl CHUNK_SHIFT)] = true
}
}
}
}

View File

@ -0,0 +1,144 @@
package ru.dbotthepony.kstarbound.world.phys
import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.ChunkTile
import ru.dbotthepony.kvector.vector.nint.Vector2i
class RectTileFlooderSizeFirst(
private val tiles: Array<ChunkTile?>,
private val seen: BooleanArray,
private val rootx: Int,
private val rooty: Int
) {
val mins: Vector2i
val maxs: Vector2i
private fun filled(x: Int, y: Int): Boolean {
if (x < 0 || x > CHUNK_SIZE_FF) {
return false
}
if (y < 0 || y > CHUNK_SIZE_FF) {
return false
}
return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x or (y shl CHUNK_SHIFT)] != null
}
private var widthPositive = 0
private var widthNegative = 0
private var heightPositive = 0
private var heightNegative = 0
private fun checkLeft(): Boolean {
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx - widthNegative, rooty + i)) {
return false
}
}
return true
}
private fun checkRight(): Boolean {
for (i in -heightNegative .. heightPositive) {
if (!filled(rootx + widthPositive, rooty + i)) {
return false
}
}
return true
}
private fun checkUp(): Boolean {
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty + heightPositive)) {
return false
}
}
return true
}
private fun checkDown(): Boolean {
for (i in -widthNegative .. widthPositive) {
if (!filled(rootx + i, rooty - heightNegative)) {
return false
}
}
return true
}
init {
var expanded = true
var hitLeft = false
var hitRight = false
var hitUp = false
var hitDown = false
while (expanded) {
expanded = false
// expand left
if (!hitLeft) {
widthNegative++
if (!checkLeft()) {
widthNegative--
hitLeft = true
} else {
expanded = true
}
}
// expand up
if (!hitUp) {
heightPositive++
if (!checkUp()) {
heightPositive--
hitUp = true
} else {
expanded = true
}
}
// expand right
if (!hitRight) {
widthPositive++
if (!checkRight()) {
widthPositive--
hitRight = true
} else {
expanded = true
}
}
// expand down
if (!hitDown) {
heightNegative++
if (!checkDown()) {
heightNegative--
hitDown = true
} else {
expanded = true
}
}
}
mins = Vector2i(rootx - widthNegative, rooty - heightNegative)
maxs = Vector2i(rootx + widthPositive, rooty + heightPositive)
}
fun markSeen() {
for (x in mins.x .. maxs.x) {
for (y in mins.y .. maxs.y) {
seen[x or (y shl CHUNK_SHIFT)] = true
}
}
}
}

View File

@ -0,0 +1,74 @@
package ru.dbotthepony.kstarbound.world.phys
import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.ChunkTile
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
private data class TileExposure(
var pos: Vector2d,
val left: Boolean,
val right: Boolean,
val up: Boolean,
val down: Boolean,
)
private class TileFlooder(
private val tiles: Array<ChunkTile?>,
private val seen: BooleanArray,
rootx: Int,
rooty: Int
) {
/**
* Tile positions which have at least one face free of neighbours
*/
val exposed = ArrayList<TileExposure>()
private fun get(x: Int, y: Int): Boolean {
if (x < 0 || x > CHUNK_SIZE_FF) {
return false
}
if (y < 0 || y > CHUNK_SIZE_FF) {
return false
}
return tiles[x or (y shl CHUNK_SHIFT)] != null
}
private fun visit(x: Int, y: Int) {
if (seen[x or (y shl CHUNK_SHIFT)])
return
seen[x or (y shl CHUNK_SHIFT)] = true
val left = get(x - 1, y)
val right = get(x + 1, y)
val up = get(x, y + 1)
val down = get(x, y - 1)
if (!left || !right || !up || !down) {
exposed.add(TileExposure(Vector2d(x.toDouble() + 0.5, y.toDouble() + 0.5), !left, !right, !up, !down))
}
if (left) {
visit(x - 1, y)
}
if (right) {
visit(x + 1, y)
}
if (up) {
visit(x, y + 1)
}
if (down) {
visit(x, y - 1)
}
}
init {
visit(rootx, rooty)
}
}