diff --git a/build.gradle.kts b/build.gradle.kts index ba812533..4ff971b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation("com.github.jnr:jnr-ffi:2.2.13") implementation("ru.dbotthepony:kbox2d:2.4.1.6") - implementation("ru.dbotthepony:kvector:2.2.8") + implementation("ru.dbotthepony:kvector:2.2.9") implementation("com.github.ben-manes.caffeine:caffeine:3.1.5") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 3ff5cd97..6836f8ca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -13,7 +13,6 @@ import ru.dbotthepony.kstarbound.player.QuestDescriptor import ru.dbotthepony.kstarbound.player.QuestInstance import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.world.ChunkPos -import ru.dbotthepony.kstarbound.world.TileState import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kvector.vector.Vector2d @@ -90,29 +89,7 @@ fun main() { for (y in 0 .. 31) { for (x in 0 .. 31) { - chunk.foreground.setTile(x, y, TileState.read(starbound.tilesByID::get, starbound.tileModifiersByID::get, reader)) - chunk.background.setTile(x, y, TileState.read(starbound.tilesByID::get, starbound.tileModifiersByID::get, reader)) - - val liquid = reader.readUnsignedByte() - val liquidLevel = reader.readFloat() - val liquidPressure = reader.readFloat() - val liquidIsInfinite = reader.readBoolean() - val collisionMap = reader.readUnsignedByte() - val dungeonId = reader.readUnsignedShort() - val biome = reader.readUnsignedByte() - val envBiome = reader.readUnsignedByte() - val indestructible = reader.readBoolean() - val unknown = reader.readUnsignedByte() - - val getLiquid = starbound.liquidByID[liquid] - - if (getLiquid != null) { - val state = chunk.setLiquid(x, y, getLiquid.value)!! - - state.isInfinite = liquidIsInfinite - state.pressure = liquidPressure - state.level = liquidLevel - } + chunk.getCell(x, y).read(starbound.tilesByID::get, starbound.tileModifiersByID::get, starbound.liquidByID::get, reader) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt index 44394c4b..531f3cf0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -23,6 +23,7 @@ import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector3f import java.io.Closeable import java.util.LinkedList +import java.util.function.IntSupplier /** * Псевдо zPos у фоновых тайлов @@ -36,12 +37,14 @@ const val Z_LEVEL_LIQUID = 10000 class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk(world, pos), Closeable { val state: GLStateTracker get() = world.client.gl - private inner class TileLayerRenderer(private val layerChangeset: () -> Int, private val isBackground: Boolean) : AutoCloseable { + private inner class TileLayerRenderer(private val layerChangeset: IntSupplier, private val view: () -> ITileChunk, private val isBackground: Boolean) : AutoCloseable { private val layers = TileLayerList() val bakedMeshes = LinkedList>() private var changeset = -1 - fun bake(view: ITileChunk) { + fun bake() { + val view = view() + if (state.isSameThread()) { for (mesh in bakedMeshes) { mesh.first.close() @@ -67,7 +70,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk ITileChunk) { - if (changeset != layerChangeset.invoke()) { - this.bake(provider.invoke()) - changeset = layerChangeset.invoke() + fun autoBake() { + if (changeset != layerChangeset.asInt) { + this.bake() + changeset = layerChangeset.asInt } } @@ -123,13 +128,13 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk ITileChunk) { - autoBake(provider) + fun autoBakeAndUpload() { + autoBake() autoUpload() } - fun bakeAndRender(transform: Matrix4fStack, provider: () -> ITileChunk) { - autoBakeAndUpload(provider) + fun bakeAndRender(transform: Matrix4fStack) { + autoBakeAndUpload() render(transform) } @@ -143,16 +148,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk equalityTester.test(getter.getTile(thisPos), getter.getTile(thisPos + offsetPos)) "Connects" -> getter.getTile(thisPos + offsetPos).material != null @@ -53,7 +53,7 @@ data class RenderRuleList( } } - fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { + fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { if (inverse) { return !doTest(getter, equalityTester, thisPos, offsetPos) } @@ -67,7 +67,7 @@ data class RenderRuleList( } } - fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { + fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { when (join) { Combination.ALL -> { for (entry in entries) { @@ -120,7 +120,7 @@ data class RenderMatch( ) { var rule by WriteOnce() - fun test(getter: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { + fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { return rule.test(getter, equalityTester, thisPos, offset) } @@ -156,7 +156,7 @@ data class RenderMatch( * * [equalityTester] требуется для проверки раенства между "этим" тайлом и другим */ - fun test(tileAccess: ITileGetter, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { + fun test(tileAccess: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { for (matcher in matchAllPoints) { if (!matcher.test(tileAccess, equalityTester, thisPos)) { return false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt new file mode 100644 index 00000000..8dc92335 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/CellView.kt @@ -0,0 +1,62 @@ +package ru.dbotthepony.kstarbound.world + +/** + * Предоставляет доступ к чанку и его соседям + * + * В основном для использования в местах, где нужен не мир, а определённый чанк мира, + * и при этом координаты проверяются относительно чанка и могут спокойно выйти за его пределы, + * с желанием получить тайл из соседнего чанка + */ +class CellView( + override val pos: ChunkPos, + val center: IChunk?, + + val right: IChunk?, + val top: IChunk?, + val topRight: IChunk?, + val topLeft: IChunk?, + + val left: IChunk?, + val bottom: IChunk?, + val bottomLeft: IChunk?, + val bottomRight: IChunk?, +) : IChunk { + val backgroundView = BackgroundView(this) + val foregroundView = ForegroundView(this) + + override fun getCell(x: Int, y: Int): IChunkCell { + if (x in 0 ..CHUNK_SIZE_FF) { + if (y in 0 ..CHUNK_SIZE_FF) { + return center?.getCell(x, y) ?: IChunkCell.Companion + } + + if (y < 0) { + return bottom?.getCell(x, y + CHUNK_SIZE) ?: IChunkCell.Companion + } else { + return top?.getCell(x, y - CHUNK_SIZE) ?: IChunkCell.Companion + } + } + + if (x < 0) { + if (y in 0 ..CHUNK_SIZE_FF) { + return left?.getCell(x + CHUNK_SIZE, y) ?: IChunkCell.Companion + } + + if (y < 0) { + return bottomLeft?.getCell(x + CHUNK_SIZE, y + CHUNK_SIZE) ?: IChunkCell.Companion + } else { + return topLeft?.getCell(x + CHUNK_SIZE, y - CHUNK_SIZE) ?: IChunkCell.Companion + } + } else { + if (y in 0 ..CHUNK_SIZE_FF) { + return right?.getCell(x - CHUNK_SIZE, y) ?: IChunkCell.Companion + } + + if (y < 0) { + return bottomRight?.getCell(x - CHUNK_SIZE, y + CHUNK_SIZE) ?: IChunkCell.Companion + } else { + return topRight?.getCell(x - CHUNK_SIZE, y - CHUNK_SIZE) ?: IChunkCell.Companion + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 1ff7906a..3c7f4efe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -5,6 +5,8 @@ import ru.dbotthepony.kbox2d.api.FixtureDef import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.dynamics.B2Fixture 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.util.TwoDimensionalArray import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.phys.RectTileFlooderDepthFirst @@ -26,24 +28,22 @@ import kotlin.collections.HashSet * * Весь игровой мир будет измеряться в Starbound Unit'ах */ -abstract class Chunk, This : Chunk>(val world: WorldType, val pos: ChunkPos) { - /** - * Возвращает общий счётчик изменений чанка - */ +abstract class Chunk, This : Chunk>(val world: WorldType, final override val pos: ChunkPos) : IChunk { var changeset = 0 private set - - /** - * Возвращает счётчик изменений чанка по тайлам - */ 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 val left get() = pos.left val right get() = pos.right @@ -66,173 +66,232 @@ abstract class Chunk, This : Chunk() + private val collisionCacheView = Collections.unmodifiableCollection(collisionCache) + + inner class Cell(val x: Int, val y: Int) : IChunkCell { + inner class Tile(private val foreground: Boolean) : ITileState { + private fun change() { + changeset++ + cellChangeset++ + tileChangeset++ + + if (foreground) { + collisionChangeset++ + foregroundChangeset++ + markPhysicsDirty() + } else { + backgroundChangeset++ } } - var level: Float = 1f - set(value) { - if (value != field) { - field = value - liquidChangeset++ - changeset++ - } - } - - var isInfinite: Boolean = false - set(value) { - if (value != field) { - field = value - liquidChangeset++ - changeset++ - } - } - } - - inner class TileLayer : IMutableTileChunk { - /** - * Возвращает счётчик изменений этого слоя - */ - var changeset = 0 - private set - - private val collisionCache = ArrayList() - private val collisionCacheView = Collections.unmodifiableCollection(collisionCache) - private var collisionChangeset = -1 - - private val body = world.physics.createBody(BodyDef( - position = pos.firstBlock.toDoubleVector(), - userData = this - )) - - private val collisionChains = ArrayList() - - 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_SHIFT)] && tiles[x, y].material != null) { - val depthFirst = RectTileFlooderDepthFirst(tiles, seen, x, y) - val sizeFirst = RectTileFlooderSizeFirst(tiles, 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) + override var material: TileDefinition? = 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() + } + } + + override val isEmpty: Boolean + get() = false } - /** - * Возвращает список AABB тайлов этого слоя - * - * Данный список напрямую указывает на внутреннее состояние и будет изменён при перестройке - * коллизии чанка, поэтому если необходим стабильный список, его необходимо скопировать - */ - fun collisionLayers(): Collection { - if (collisionChangeset != changeset) { - bakeCollisions() + inner class Liquid : ILiquidState { + private fun change() { + changeset++ + cellChangeset++ + liquidChangeset++ } - return collisionCacheView + override var def: LiquidDefinition? = null + set(value) { + if (field !== value) { + field = value + change() + } + } + + override var level: Float = 0f + set(value) { + if (field != value) { + field = value + change() + } + } + + override var pressure: Float = 0f + set(value) { + if (field != value) { + field = value + change() + } + } + + override var isInfinite: Boolean = false + set(value) { + if (field != value) { + field = value + change() + } + } + + override val isEmpty: Boolean + get() = false } - override val pos: ChunkPos - get() = this@Chunk.pos - - inline val left get() = pos.left - inline val right get() = pos.right - inline val top get() = pos.top - inline val bottom get() = pos.bottom - inline val topLeft get() = pos.topLeft - inline val topRight get() = pos.topRight - inline val bottomLeft get() = pos.bottomLeft - inline val bottomRight get() = pos.bottomRight - - /** - * Хранит тайлы как x + y * CHUNK_SIZE - */ - private val tiles: Object2DArray = Object2DArray(CHUNK_SIZE, CHUNK_SIZE, TileState.EMPTY) - - override fun getTile(x: Int, y: Int): TileState { - return tiles[x, y] - } - - override fun setTile(x: Int, y: Int, value: TileState) { - tiles[x, y] = value - tileChangeset++ + private fun change() { changeset++ - collisionChangeset++ - this@Chunk.changeset++ - markPhysicsDirty() + cellChangeset++ } - override fun randomLongFor(x: Int, y: Int): Long { - return super.randomLongFor(x, y) xor world.seed + 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 + change() + } + } + + override var biome: Int = 0 + set(value) { + if (field != value) { + field = value + change() + } + } + + override var envBiome: Int = 0 + set(value) { + if (field != value) { + field = value + change() + } + } + + override var isIndestructible: Boolean = false + set(value) { + if (field != value) { + field = value + change() + } + } + + override val isEmpty: Boolean + get() = false + } + + private val body = world.physics.createBody(BodyDef( + position = pos.firstBlock.toDoubleVector(), + userData = this + )) + + private val collisionChains = ArrayList() + + 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_SHIFT)] && cells[x, y].foreground.material != null) { + val depthFirst = RectTileFlooderDepthFirst(cells, seen, x, y) + val sizeFirst = RectTileFlooderSizeFirst(cells, 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) + } + } } } - val foreground = TileLayer() - val background = TileLayer() + /** + * Возвращает список AABB тайлов этого слоя + * + * Данный список напрямую указывает на внутреннее состояние и будет изменён при перестройке + * коллизии чанка, поэтому если необходим стабильный список, его необходимо скопировать + */ + fun collisionLayers(): Collection { + bakeCollisions() + return collisionCacheView + } - protected val liquidStates: Object2DArray = Object2DArray.nulls(CHUNK_SIZE, CHUNK_SIZE) - - fun getLiquid(x: Int, y: Int) = liquidStates[x, y] - - fun setLiquid(x: Int, y: Int, value: LiquidDefinition?): LiquidState? { - if (value == null) { - liquidStates.set(x, y, null) - changeset++ - liquidChangeset++ - return null - } - - val state = LiquidState(value) - liquidStates[x, y] = state - changeset++ - liquidChangeset++ - return state + override fun randomLongFor(x: Int, y: Int): Long { + return super.randomLongFor(x, y) xor world.seed } protected val entities = HashSet() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunks.kt similarity index 63% rename from src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunks.kt index d0ddb409..9939bbc3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/ChunkAPI.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunks.kt @@ -10,69 +10,13 @@ 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 { - /** - * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка - */ - fun getTile(x: Int, y: Int): TileState - - /** - * Возвращает тайл по ОТНОСИТЕЛЬНЫМ координатам внутри чанка - */ - fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2()) -} - -interface ITileSetter : ITileMap { - fun setTile(x: Int, y: Int, value: TileState) - fun setTile(pos: IStruct2i, value: TileState) = setTile(pos.component1(), pos.component2(), value) -} - -fun ITileGetter.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator> { - return object : Iterator> { - private var x = fromX - private var y = fromY - - override fun hasNext(): Boolean { - return x < toX && y < toY - } - - override fun next(): Pair { - if (!hasNext()) - throw NoSuchElementException() - - val tile = getTile(x, y) - val pos = Vector2i(x, y) - - x++ - - if (x >= toX) { - y++ - x = 0 - } - - return pos to tile - } - } -} - -/** - * Интерфейс предоставляет из себя описание класса, который имеет координаты чанка - */ -interface IChunkPositionable : ITileMap { +interface IChunk { val pos: ChunkPos + // relative + fun getCell(x: Int, y: Int): IChunkCell + fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2()) + /** * Возвращает псевдослучайное Long для заданной позиции * @@ -111,5 +55,77 @@ interface IChunkPositionable : ITileMap { fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) } -interface ITileChunk : ITileGetter, IChunkPositionable -interface IMutableTileChunk : ITileChunk, ITileSetter +// for getting tiles directly, avoiding manual layer specification +interface ITileChunk : IChunk { + // relative + fun getTile(x: Int, y: Int): ITileState + fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2()) +} + +class ForegroundView(private val parent: IChunk) : ITileChunk, IChunk by parent { + override fun getTile(x: Int, y: Int): ITileState { + return parent.getCell(x, y).foreground + } +} + +class BackgroundView(private val parent: IChunk) : ITileChunk, IChunk by parent { + override fun getTile(x: Int, y: Int): ITileState { + return parent.getCell(x, y).background + } +} + +fun IChunk.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator> { + return object : Iterator> { + private var x = fromX + private var y = fromY + + override fun hasNext(): Boolean { + return x < toX && y < toY + } + + override fun next(): Pair { + if (!hasNext()) + throw NoSuchElementException() + + val tile = getCell(x, y) + val pos = Vector2i(x, y) + + x++ + + if (x >= toX) { + y++ + x = 0 + } + + return pos to tile + } + } +} + +fun ITileChunk.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator> { + return object : Iterator> { + private var x = fromX + private var y = fromY + + override fun hasNext(): Boolean { + return x < toX && y < toY + } + + override fun next(): Pair { + if (!hasNext()) + throw NoSuchElementException() + + val tile = getTile(x, y) + val pos = Vector2i(x, y) + + x++ + + if (x >= toX) { + y++ + x = 0 + } + + return pos to tile + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/RigidTileView.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/RigidTileView.kt deleted file mode 100644 index f9328107..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/RigidTileView.kt +++ /dev/null @@ -1,43 +0,0 @@ -package ru.dbotthepony.kstarbound.world - -import ru.dbotthepony.kstarbound.util.NotNullTwoDimensionalArray -import ru.dbotthepony.kvector.arrays.Object2DArray - -/** - * Предоставляет доступ к чанку и его соседям - * - * Данный вариант отличается от [TileView] тем, что данный класс *копирует* список тайлов из всех чанков, которые ему известны - * в единый массив. - * - * Полезно в ситуациях, когда необходимо максимально быстрое получение данных о тайлах. - */ -class RigidTileView( - view: TileView, -) : ITileChunk { - constructor( - pos: ChunkPos, - center: ITileChunk?, - - right: ITileChunk?, - top: ITileChunk?, - topRight: ITileChunk?, - topLeft: ITileChunk?, - - left: ITileChunk?, - bottom: ITileChunk?, - bottomLeft: ITileChunk?, - bottomRight: ITileChunk?, - ) : this(TileView(pos, center, right, top, topRight, topLeft, left, bottom, bottomLeft, bottomRight)) - - override val pos: ChunkPos = view.pos - - private val memory: Object2DArray - - init { - memory = Object2DArray(CHUNK_SIZE * 3, CHUNK_SIZE * 3) { a, b -> view.getTile(a - CHUNK_SIZE, b - CHUNK_SIZE) } - } - - override fun getTile(x: Int, y: Int): TileState { - return memory[x + CHUNK_SIZE, y + CHUNK_SIZE] - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileState.kt deleted file mode 100644 index 5906d698..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileState.kt +++ /dev/null @@ -1,86 +0,0 @@ -package ru.dbotthepony.kstarbound.world - -import ru.dbotthepony.kstarbound.RegistryObject -import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier -import ru.dbotthepony.kstarbound.defs.tile.TileDefinition -import java.io.DataInputStream - -enum class TileColor { - DEFAULT, - RED, - BLUE, - GREEN, - YELLOW, - BROWN, - PURPLE, - BLACK, - WHITE; - - companion object { - private val values = values() - - fun of(index: Int): TileColor { - return values.getOrElse(index) { DEFAULT } - } - } -} - -data class TileState( - val material: TileDefinition? = null, - val modifier: MaterialModifier? = null, - val color: TileColor = TileColor.DEFAULT, - val hueShift: Float = 0f, - val modifierHueShift: Float = 0f, -) { - private fun with( - material: TileDefinition? = this.material, - modifier: MaterialModifier? = this.modifier, - color: TileColor = this.color, - hueShift: Float = this.hueShift, - modifierHueShift: Float = this.modifierHueShift, - ): TileState { - if (this.material === material && this.modifier === modifier && this.color == color && this.hueShift == hueShift && this.modifierHueShift == modifierHueShift) { - return this - } else { - return TileState(material, modifier, color, hueShift, modifierHueShift) - } - } - - fun withHueShift(hueShift: Int): TileState { - if (hueShift < 0) { - return with(hueShift = 0f) - } else if (hueShift >= 255) { - return with(hueShift = 360f) - } else { - return with(hueShift = hueShift / 255f * 360f) - } - } - - fun withModifierHueShift(hueShift: Int): TileState { - if (hueShift < 0) { - return with(modifierHueShift = 0f) - } else if (hueShift >= 255) { - return with(modifierHueShift = 360f) - } else { - return with(modifierHueShift = hueShift / 255f * 360f) - } - } - - companion object { - val EMPTY = TileState() - - fun read( - materialAccess: (Int) -> RegistryObject?, - modifierAccess: (Int) -> RegistryObject?, - stream: DataInputStream - ): TileState { - val mat = materialAccess(stream.readUnsignedShort()) - val hue = stream.read() - val color = stream.read() - val mod = modifierAccess(stream.readUnsignedShort()) - val modHue = stream.read() - - return TileState(mat?.value, mod?.value, TileColor.of(color), hue / 255f * 360f, modHue / 255f * 360f) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt deleted file mode 100644 index 42e929a0..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileView.kt +++ /dev/null @@ -1,78 +0,0 @@ -package ru.dbotthepony.kstarbound.world - -/** - * Предоставляет доступ к чанку и его соседям - * - * В основном для использования в местах, где нужен не мир, а определённый чанк мира, - * и при этом координаты проверяются относительно чанка и могут спокойно выйти за его пределы, - * с желанием получить тайл из соседнего чанка - */ -open class TileView( - override val pos: ChunkPos, - 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 getTile(x: Int, y: Int): TileState { - if (x in 0 ..CHUNK_SIZE_FF) { - if (y in 0 ..CHUNK_SIZE_FF) { - return center?.getTile(x, y) ?: TileState.EMPTY - } - - if (y < 0) { - return bottom?.getTile(x, y + CHUNK_SIZE) ?: TileState.EMPTY - } else { - return top?.getTile(x, y - CHUNK_SIZE) ?: TileState.EMPTY - } - } - - if (x < 0) { - if (y in 0 ..CHUNK_SIZE_FF) { - return left?.getTile(x + CHUNK_SIZE, y) ?: TileState.EMPTY - } - - if (y < 0) { - return bottomLeft?.getTile(x + CHUNK_SIZE, y + CHUNK_SIZE) ?: TileState.EMPTY - } else { - return topLeft?.getTile(x + CHUNK_SIZE, y - CHUNK_SIZE) ?: TileState.EMPTY - } - } else { - if (y in 0 ..CHUNK_SIZE_FF) { - return right?.getTile(x - CHUNK_SIZE, y) ?: TileState.EMPTY - } - - if (y < 0) { - return bottomRight?.getTile(x - CHUNK_SIZE, y + CHUNK_SIZE) ?: TileState.EMPTY - } else { - return topRight?.getTile(x - CHUNK_SIZE, y - CHUNK_SIZE) ?: TileState.EMPTY - } - } - } -} - -class MutableTileView( - pos: ChunkPos, - 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(pos, center, right, top, topRight, topLeft, left, bottom, bottomLeft, bottomRight), IMutableTileChunk { - override fun setTile(x: Int, y: Int, value: TileState) { - TODO() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Tiles.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Tiles.kt new file mode 100644 index 00000000..03874f72 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Tiles.kt @@ -0,0 +1,181 @@ +package ru.dbotthepony.kstarbound.world + +import ru.dbotthepony.kstarbound.RegistryObject +import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition +import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import java.io.DataInputStream + +enum class TileColor { + DEFAULT, + RED, + BLUE, + GREEN, + YELLOW, + BROWN, + PURPLE, + BLACK, + WHITE; + + companion object { + private val values = values() + + fun of(index: Int): TileColor { + return values.getOrElse(index) { DEFAULT } + } + } +} + +interface ITileState { + var material: TileDefinition? + var modifier: MaterialModifier? + var color: TileColor + var hueShift: Float + var modifierHueShift: Float + + val isEmpty: Boolean + + fun setHueShift(value: Int) { + if (value < 0) { + hueShift = 0f + } else if (value >= 255) { + hueShift = 360f + } else { + hueShift = value / 255f * 360f + } + } + + fun setModHueShift(value: Int) { + if (value < 0) { + modifierHueShift = 0f + } else if (value >= 255) { + modifierHueShift = 360f + } else { + modifierHueShift = value / 255f * 360f + } + } + + fun read( + materialAccess: (Int) -> RegistryObject?, + modifierAccess: (Int) -> RegistryObject?, + stream: DataInputStream + ) { + material = materialAccess(stream.readUnsignedShort())?.value + setHueShift(stream.read()) + color = TileColor.of(stream.read()) + modifier = modifierAccess(stream.readUnsignedShort())?.value + setModHueShift(stream.read()) + } + + companion object : ITileState { + override var material: TileDefinition? + get() = null + set(value) { throw UnsupportedOperationException("Empty tile state") } + override var modifier: MaterialModifier? + get() = null + set(value) { throw UnsupportedOperationException("Empty tile state") } + override var color: TileColor + get() = TileColor.DEFAULT + set(value) { throw UnsupportedOperationException("Empty tile state") } + override var hueShift: Float + get() = 0f + set(value) { throw UnsupportedOperationException("Empty tile state") } + override var modifierHueShift: Float + get() = 0f + set(value) { throw UnsupportedOperationException("Empty tile state") } + override val isEmpty: Boolean + get() = true + } +} + +interface ILiquidState { + var def: LiquidDefinition? + var level: Float + var pressure: Float + var isInfinite: Boolean + + val isEmpty: Boolean + + fun read( + liquidAccess: (Int) -> RegistryObject?, + stream: DataInputStream + ) { + def = liquidAccess(stream.readUnsignedByte())?.value + level = stream.readFloat() + pressure = stream.readFloat() + isInfinite = stream.readBoolean() + } + + companion object : ILiquidState { + override var def: LiquidDefinition? + get() = null + set(value) { throw UnsupportedOperationException("Empty liquid state") } + override var level: Float + get() = 0f + set(value) { throw UnsupportedOperationException("Empty liquid state") } + override var pressure: Float + get() = 0f + set(value) { throw UnsupportedOperationException("Empty liquid state") } + override var isInfinite: Boolean + get() = false + set(value) { throw UnsupportedOperationException("Empty liquid state") } + override val isEmpty: Boolean + get() = true + } +} + +interface IChunkCell { + val foreground: ITileState + val background: ITileState + val liquid: ILiquidState + + var dungeonId: Int + var biome: Int + var envBiome: Int + var isIndestructible: Boolean + + val isEmpty: Boolean + + fun read( + materialAccess: (Int) -> RegistryObject?, + modifierAccess: (Int) -> RegistryObject?, + liquidAccess: (Int) -> RegistryObject?, + stream: DataInputStream + ) { + foreground.read(materialAccess, modifierAccess, stream) + background.read(materialAccess, modifierAccess, stream) + liquid.read(liquidAccess, stream) + + stream.skipBytes(1) // collisionMap + + dungeonId = stream.readUnsignedShort() + biome = stream.readUnsignedByte() + envBiome = stream.readUnsignedByte() + isIndestructible = stream.readBoolean() + + stream.skipBytes(1) // unknown + } + + companion object : IChunkCell { + override val foreground: ITileState + get() = ITileState.Companion + override val background: ITileState + get() = ITileState.Companion + override val liquid: ILiquidState + get() = ILiquidState.Companion + override var dungeonId: Int + get() = 0 + set(value) { throw UnsupportedOperationException("Empty chunk cell") } + override var biome: Int + get() = 0 + set(value) { throw UnsupportedOperationException("Empty chunk cell") } + override var envBiome: Int + get() = 0 + set(value) { throw UnsupportedOperationException("Empty chunk cell") } + override var isIndestructible: Boolean + get() = false + set(value) { throw UnsupportedOperationException("Empty chunk cell") } + override val isEmpty: Boolean + get() = true + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 0d3a3f18..f55e392c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,8 +1,8 @@ package ru.dbotthepony.kstarbound.world import com.google.common.collect.ImmutableList -import it.unimi.dsi.fastutil.objects.Object2ObjectFunction -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.longs.Long2ObjectFunction +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import ru.dbotthepony.kbox2d.api.ContactImpulse import ru.dbotthepony.kbox2d.api.IContactFilter import ru.dbotthepony.kbox2d.api.IContactListener @@ -29,8 +29,8 @@ import kotlin.math.sin const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT data class RayCastResult( - val traversedTiles: List>, - val hitTile: Pair?, + val traversedTiles: List>, + val hitTile: Pair?, val fraction: Double ) @@ -88,7 +88,7 @@ enum class RayFilterResult { } fun interface TileRayFilter { - fun test(state: TileState, fraction: Double, position: Vector2i): RayFilterResult + fun test(state: IChunkCell, fraction: Double, position: Vector2i): RayFilterResult } /** @@ -99,23 +99,23 @@ val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINU /** * Попадает по первому не-пустому тайлу */ -val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material != null) } +val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material != null) } /** * Попадает по первому пустому тайлу */ -val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material == null) } +val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material == null) } /** * Попадает по первому тайлу который блокирует проход света */ -val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.material?.renderParameters?.lightTransparent == false) } +val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material?.renderParameters?.lightTransparent == false) } abstract class World, ChunkType : Chunk>( val seed: Long, val widthInChunks: Int ) { - protected val chunkMap = Object2ObjectOpenHashMap() + protected val chunkMap = Long2ObjectOpenHashMap() /** * Является ли мир "сферическим" @@ -300,12 +300,12 @@ abstract class World, ChunkType : Chunk? { @@ -319,7 +319,7 @@ abstract class World, ChunkType : Chunk() @@ -336,7 +336,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk>() + val tiles = LinkedList>() var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE) - var hitTile: Pair? = null + var hitTile: Pair? = null while (t < 1.0) { val (x, y) = rayStart + dir * t val tilePos = Vector2i(x.roundToInt(), y.roundToInt()) if (tilePos != prev) { - val tile = getTile(tilePos) ?: TileState.EMPTY + val tile = getCell(tilePos) ?: IChunkCell.Companion when (filter.test(tile, t, tilePos)) { RayFilterResult.HIT -> { @@ -449,7 +445,7 @@ abstract class World, ChunkType : Chunk - if (state.material?.renderParameters?.lightTransparent == false) { + if (state.foreground.material?.renderParameters?.lightTransparent == false) { currentIntensity -= falloffByTile } else { currentIntensity -= falloffByTravel @@ -545,56 +541,23 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk val a = o1.distance(worldaabb) @@ -666,7 +629,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk, + private val tiles: Object2DArray, private val seen: BooleanArray, rootx: Int, rooty: Int @@ -24,7 +24,7 @@ class RectTileFlooderDepthFirst( return false } - return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x, y].material != null + return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x, y].foreground.material != null } init { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt index 71cb00e5..82bd9590 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/RectTileFlooderSizeFirst.kt @@ -2,12 +2,12 @@ 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.TileState +import ru.dbotthepony.kstarbound.world.IChunkCell import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.vector.Vector2i class RectTileFlooderSizeFirst( - private val tiles: Object2DArray, + private val tiles: Object2DArray, private val seen: BooleanArray, private val rootx: Int, private val rooty: Int @@ -24,7 +24,7 @@ class RectTileFlooderSizeFirst( return false } - return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x, y].material != null + return !seen[x or (y shl CHUNK_SHIFT)] && tiles[x, y].foreground.material != null } private var widthPositive = 0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt deleted file mode 100644 index e4979459..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/phys/TileFlooder.kt +++ /dev/null @@ -1,75 +0,0 @@ -package ru.dbotthepony.kstarbound.world.phys - -import ru.dbotthepony.kstarbound.util.NotNullTwoDimensionalArray -import ru.dbotthepony.kstarbound.world.CHUNK_SHIFT -import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF -import ru.dbotthepony.kstarbound.world.TileState -import ru.dbotthepony.kvector.vector.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: NotNullTwoDimensionalArray, - private val seen: BooleanArray, - rootx: Int, - rooty: Int -) { - /** - * Tile positions which have at least one face free of neighbours - */ - val exposed = ArrayList() - - 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, y].material != 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) - } -}