Arbitrary dimension chunks, null/not null cell distinction

This commit is contained in:
DBotThePony 2023-09-05 16:09:09 +07:00
parent fb44d77353
commit 94fe3662ad
Signed by: DBot
GPG Key ID: DCC23B5715498507
23 changed files with 444 additions and 507 deletions

View File

@ -13,6 +13,7 @@ import ru.dbotthepony.kstarbound.player.QuestDescriptor
import ru.dbotthepony.kstarbound.player.QuestInstance import ru.dbotthepony.kstarbound.player.QuestInstance
import ru.dbotthepony.kstarbound.util.JVMTimeSource import ru.dbotthepony.kstarbound.util.JVMTimeSource
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
@ -74,7 +75,6 @@ fun main() {
find += System.currentTimeMillis() - t find += System.currentTimeMillis() - t
if (data != null) { if (data != null) {
val chunk = client.world!!.chunkMap.computeIfAbsent(ChunkPos(chunkX, chunkY))
val inflater = Inflater() val inflater = Inflater()
inflater.setInput(data) inflater.setInput(data)
@ -90,7 +90,13 @@ fun main() {
for (y in 0 .. 31) { for (y in 0 .. 31) {
for (x in 0 .. 31) { for (x in 0 .. 31) {
chunk.getCell(x, y).read(starbound.tilesByID::get, starbound.tileModifiersByID::get, starbound.liquidByID::get, reader) val cell = client.world!!.chunkMap.getCellDirect(chunkX * 32 + x, chunkY * 32 + y)
if (cell == null) {
IChunkCell.skip(reader)
} else {
cell.read(starbound.tilesByID::get, starbound.tileModifiersByID::get, starbound.liquidByID::get, reader)
}
} }
} }
@ -148,14 +154,13 @@ fun main() {
//ent.position += Vector2d(y = 14.0, x = -10.0) //ent.position += Vector2d(y = 14.0, x = -10.0)
ent.position = Vector2d(600.0 + 16.0, 721.0 + 48.0) ent.position = Vector2d(600.0 + 16.0, 721.0 + 48.0)
client.camera.pos = Vector2f(-2967f, 685f) client.camera.pos = Vector2f(238f, 685f)
client.onDrawGUI { client.onDrawGUI {
client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f) client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f)
client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f) client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f)
client.gl.font.render("${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f) client.gl.font.render("Camera: ${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f)
client.gl.font.render("Camera: ${ChunkPos.fromPosition(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) client.gl.font.render("World chunk: ${client.world!!.chunkMap.cellToChunk(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f)
client.gl.font.render("World: ${client.world!!.chunkMap.cellToGrid(client.camera.pos.toDoubleVector())}", y = 180f, scale = 0.25f)
} }
client.onPreDrawWorld { client.onPreDrawWorld {

View File

@ -8,14 +8,12 @@ import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.TileLayerList import ru.dbotthepony.kstarbound.client.render.TileLayerList
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZEd
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEf
import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.arrays.Matrix4fStack
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
import ru.dbotthepony.kvector.vector.Vector2i
import java.io.Closeable import java.io.Closeable
import java.util.LinkedList import java.util.LinkedList
@ -28,13 +26,13 @@ import java.util.LinkedList
const val Z_LEVEL_BACKGROUND = 60000 const val Z_LEVEL_BACKGROUND = 60000
const val Z_LEVEL_LIQUID = 10000 const val Z_LEVEL_LIQUID = 10000
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable { class ClientChunk(world: ClientWorld, pos: ChunkPos, width: Int, height: Int) : Chunk<ClientWorld, ClientChunk>(world, pos, width, height), Closeable {
val state: GLStateTracker get() = world.client.gl val state: GLStateTracker get() = world.client.gl
private inner class TileLayerRenderer(private val view: ITileAccess, private val isBackground: Boolean) : AutoCloseable { private inner class TileLayerRenderer(private val view: ITileAccess, private val isBackground: Boolean) : AutoCloseable {
private val layers = TileLayerList() private val layers = TileLayerList()
val bakedMeshes = LinkedList<Pair<ConfiguredStaticMesh, Int>>() val bakedMeshes = LinkedList<Pair<ConfiguredStaticMesh, Int>>()
var isDirty = true var isDirty = false
fun bake() { fun bake() {
if (!isDirty) return if (!isDirty) return
@ -50,19 +48,20 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
layers.clear() layers.clear()
for ((pos, tile) in view.iterateTiles()) { for (x in 0 until width) {
if (!world.chunkMap.inBounds(this@ClientChunk.pos.tile + pos)) continue for (y in 0 until height) {
val tile = view.getTile(x, y) ?: continue
val material = tile.material
val material = tile.material if (material != null) {
world.client.tileRenderers.getTileRenderer(material.materialName).tesselate(tile, view, layers, Vector2i(x, y), background = isBackground)
}
if (material != null) { val modifier = tile.modifier
world.client.tileRenderers.getTileRenderer(material.materialName).tesselate(tile, view, layers, pos, background = isBackground)
}
val modifier = tile.modifier if (modifier != null) {
world.client.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, layers, Vector2i(x, y), background = isBackground, isModifier = true)
if (modifier != null) { }
world.client.tileRenderers.getModifierRenderer(modifier.modName).tesselate(tile, view, layers, pos, background = isBackground, isModifier = true)
} }
} }
} }
@ -147,8 +146,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
liquidTypes.clear() liquidTypes.clear()
liquidTypesVer = liquidChangeset liquidTypesVer = liquidChangeset
for (x in 0 until CHUNK_SIZE) { for (x in 0 until width) {
for (y in 0 until CHUNK_SIZE) { for (y in 0 until height) {
getCell(x, y).liquid.def?.let { liquidTypes.add(it) } getCell(x, y).liquid.def?.let { liquidTypes.add(it) }
} }
} }
@ -157,7 +156,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
return liquidTypes return liquidTypes
} }
inner class Renderer(val renderOrigin: Vector2f = pos.tile.toFloatVector()) { inner class Renderer(val renderOrigin: Vector2f) {
fun addLayers(layers: LayeredRenderer) { fun addLayers(layers: LayeredRenderer) {
for ((baked, zLevel) in backgroundRenderer.bakedMeshes) { for ((baked, zLevel) in backgroundRenderer.bakedMeshes) {
layers.add(zLevel + Z_LEVEL_BACKGROUND) { layers.add(zLevel + Z_LEVEL_BACKGROUND) {
@ -200,8 +199,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
for (type in types) { for (type in types) {
builder.builder.begin() builder.builder.begin()
for (x in 0 until CHUNK_SIZE) { for (x in 0 until width) {
for (y in 0 until CHUNK_SIZE) { for (y in 0 until height) {
val state = getCell(x, y) val state = getCell(x, y)
if (state.liquid.def === type) { if (state.liquid.def === type) {

View File

@ -1,10 +1,9 @@
package ru.dbotthepony.kstarbound.client package ru.dbotthepony.kstarbound.client
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB
import ru.dbotthepony.kstarbound.math.encasingIntAABB import ru.dbotthepony.kstarbound.math.encasingIntAABB
import ru.dbotthepony.kstarbound.math.roundTowardsNegativeInfinity
import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEf
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.Vector2f import ru.dbotthepony.kvector.vector.Vector2f
@ -23,30 +22,32 @@ class ClientWorld(
} }
override fun chunkFactory(pos: ChunkPos): ClientChunk { override fun chunkFactory(pos: ChunkPos): ClientChunk {
return ClientChunk( return ClientChunk(this, pos, chunkWidth, chunkHeight)
world = this,
pos = pos,
)
} }
fun addLayers( fun addLayers(
size: AABB, size: AABB,
layers: LayeredRenderer layers: LayeredRenderer
) { ) {
for (chunk in collectWithPosition(size.encasingChunkPosAABB())) { val rx = roundTowardsNegativeInfinity(size.mins.x) / chunkWidth - 1
val (x, y) = chunk.first val ry = roundTowardsNegativeInfinity(size.mins.y) / chunkHeight - 1
val renderer = chunk.second.Renderer( val dx = roundTowardsNegativeInfinity(size.maxs.x - size.mins.x) / chunkWidth + 2
Vector2f(x * CHUNK_SIZEf + chunkMap.x.cellOffset(x), y * CHUNK_SIZEf + chunkMap.y.cellOffset(y)) val dy = roundTowardsNegativeInfinity(size.maxs.y - size.mins.y) / chunkWidth + 2
)
renderer.addLayers(layers) for (x in rx .. rx + dx) {
chunk.second.bake() for (y in ry .. ry + dy) {
val chunk = chunkMap[x, y] ?: continue
val renderer = chunk.Renderer(Vector2f(x * chunkWidth.toFloat(), y * chunkHeight.toFloat()))
renderer.addLayers(layers)
chunk.bake()
}
} }
val pos = client.screenToWorld(client.mouseCoordinatesF) val pos = client.screenToWorld(client.mouseCoordinatesF)
layers.add(-999999) { /*layers.add(-999999) {
val lightsize = 16 val lightsize = 16
val lightmap = floodLight( val lightmap = floodLight(
@ -62,7 +63,7 @@ class ClientWorld(
} }
} }
} }
} }*/
/* /*
val rayFan = ArrayList<Vector2d>() val rayFan = ArrayList<Vector2d>()

View File

@ -227,7 +227,7 @@ class StarboundClient(val starbound: Starbound) : Closeable {
val tileRenderers = TileRenderers(this) val tileRenderers = TileRenderers(this)
var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true) var world: ClientWorld? = ClientWorld(this, 0L, null, true)
init { init {
putDebugLog("Initialized OpenGL context") putDebugLog("Initialized OpenGL context")

View File

@ -168,14 +168,14 @@ class TileRenderers(val client: StarboundClient) {
private fun vertexTextureBuilder() = VertexBuilder(GLAttributeList.TILE, GeometryType.QUADS) private fun vertexTextureBuilder() = VertexBuilder(GLAttributeList.TILE, GeometryType.QUADS)
private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester { private class TileEqualityTester(val definition: TileDefinition) : EqualityRuleTester {
override fun test(thisTile: ITileState, otherTile: ITileState): Boolean { override fun test(thisTile: ITileState?, otherTile: ITileState?): Boolean {
return otherTile.material == definition && thisTile.hueShift == otherTile.hueShift return otherTile?.material == definition && thisTile?.hueShift == otherTile.hueShift
} }
} }
private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester { private class ModifierEqualityTester(val definition: MaterialModifier) : EqualityRuleTester {
override fun test(thisTile: ITileState, otherTile: ITileState): Boolean { override fun test(thisTile: ITileState?, otherTile: ITileState?): Boolean {
return otherTile.modifier == definition return otherTile?.modifier == definition
} }
} }

View File

@ -20,7 +20,7 @@ data class RenderPiece(
) )
fun interface EqualityRuleTester { fun interface EqualityRuleTester {
fun test(thisTile: ITileState, otherTile: ITileState): Boolean fun test(thisTile: ITileState?, otherTile: ITileState?): Boolean
} }
@JsonFactory @JsonFactory
@ -50,7 +50,7 @@ data class RenderRuleList(
private object Conntects : ActualTester { private object Conntects : ActualTester {
override fun doTest(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { override fun doTest(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return getter.getTile(thisPos + offsetPos).material != null return getter.getTile(thisPos + offsetPos)?.material != null
} }
} }

View File

@ -12,13 +12,6 @@ fun AABB.encasingIntAABB(): AABBi {
) )
} }
fun AABB.encasingChunkPosAABB(): AABBi {
return AABBi(
Vector2i(ChunkPos.component(roundTowardsNegativeInfinity(mins.x)), ChunkPos.component(roundTowardsNegativeInfinity(mins.y))),
Vector2i(ChunkPos.component(roundTowardsPositiveInfinity(maxs.x)), ChunkPos.component(roundTowardsPositiveInfinity(maxs.y))),
)
}
private class Iterator<T>(private val aabb: AABBi, private val factory: (x: Int, y: Int) -> T) : kotlin.collections.Iterator<T> { private class Iterator<T>(private val aabb: AABBi, private val factory: (x: Int, y: Int) -> T) : kotlin.collections.Iterator<T> {
private var x = aabb.mins.x private var x = aabb.mins.x
private var y = aabb.mins.y private var y = aabb.mins.y

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet
import ru.dbotthepony.kbox2d.api.BodyDef import ru.dbotthepony.kbox2d.api.BodyDef
import ru.dbotthepony.kbox2d.api.FixtureDef import ru.dbotthepony.kbox2d.api.FixtureDef
import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape
@ -7,10 +8,10 @@ import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_BITS
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd import ru.dbotthepony.kstarbound.world.CHUNK_SIZEd
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.api.ILiquidState import ru.dbotthepony.kstarbound.world.api.ILiquidState
@ -38,9 +39,22 @@ import kotlin.collections.HashSet
* *
* Весь игровой мир будет измеряться в Starbound Unit'ах * Весь игровой мир будет измеряться в Starbound Unit'ах
*/ */
abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(val world: WorldType, val pos: ChunkPos) : ICellAccess { abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(
val world: WorldType,
val pos: ChunkPos,
val width: Int,
val height: Int
) : ICellAccess {
var changeset = 0 var changeset = 0
private set private set(value) {
field = value
if (isEmpty) {
isEmpty = false
world.chunkMap.promote(this as This)
}
}
var tileChangeset = 0 var tileChangeset = 0
private set private set
var liquidChangeset = 0 var liquidChangeset = 0
@ -55,24 +69,21 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
protected val cells = Object2DArray(CHUNK_SIZE, CHUNK_SIZE, ::Cell) private var isEmpty = true
override fun getCell(x: Int, y: Int): IChunkCell { protected val cells by lazy {
return cells[x, y] Object2DArray.nulls<Cell>(width, height)
} }
// dimensions of this chunk with accounting for world boundaries override fun getCell(x: Int, y: Int): IChunkCell {
val inWorldWidth: Int var get = cells[x, y]
val inWorldHeight: Int
init { if (get == null) {
if (world.size == null) { get = Cell(x, y)
inWorldWidth = CHUNK_SIZE cells[x, y] = get
inWorldHeight = CHUNK_SIZE
} else {
inWorldWidth = (world.size.x - pos.tileX).coerceAtMost(CHUNK_SIZE)
inWorldHeight = (world.size.y - pos.tileY).coerceAtMost(CHUNK_SIZE)
} }
return get
} }
// local cells' tile access // local cells' tile access
@ -80,7 +91,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val localForegroundView = TileView.Foreground(this) val localForegroundView = TileView.Foreground(this)
// relative world cells access (accessing 0, 0 will lookup cell in world, relative to this chunk) // relative world cells access (accessing 0, 0 will lookup cell in world, relative to this chunk)
val worldView = OffsetCellAccess(world.chunkMap, pos) val worldView = OffsetCellAccess(world.chunkMap, pos.x * width, pos.y * height)
val worldBackgroundView = TileView.Background(worldView) val worldBackgroundView = TileView.Background(worldView)
val worldForegroundView = TileView.Foreground(worldView) val worldForegroundView = TileView.Foreground(worldView)
@ -91,10 +102,12 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
private val collisionCache = ArrayList<AABB>() private val collisionCache = ArrayList<AABB>()
private val collisionCacheView = Collections.unmodifiableCollection(collisionCache) private val collisionCacheView = Collections.unmodifiableCollection(collisionCache)
private val body = world.physics.createBody(BodyDef( private val body by lazy {
position = pos.tile.toDoubleVector(), world.physics.createBody(BodyDef(
userData = this position = pos.tile(width, height).toDoubleVector(),
)) userData = this
))
}
private val collisionChains = ArrayList<B2Fixture>() private val collisionChains = ArrayList<B2Fixture>()
@ -135,7 +148,10 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
world.dirtyPhysicsChunks.add(this as This) world.dirtyPhysicsChunks.add(this as This)
} }
inner class Cell(val x: Int, val y: Int) : IChunkCell { inner class Cell(x: Int, y: Int) : IChunkCell {
override val x: Int = x + pos.x * width
override val y: Int = y + pos.y * width
inner class Tile(private val foreground: Boolean) : ITileState { inner class Tile(private val foreground: Boolean) : ITileState {
private fun change() { private fun change() {
if (foreground) { if (foreground) {
@ -183,9 +199,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
change() change()
} }
} }
override val isEmpty: Boolean
get() = false
} }
inner class Liquid : ILiquidState { inner class Liquid : ILiquidState {
@ -226,9 +239,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
change() change()
} }
} }
override val isEmpty: Boolean
get() = false
} }
private fun change() { private fun change() {
@ -271,9 +281,6 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
change() change()
} }
} }
override val isEmpty: Boolean
get() = false
} }
fun bakeCollisions() { fun bakeCollisions() {
@ -292,11 +299,11 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
val xyAdd = Vector2d(pos.x * CHUNK_SIZEd, pos.y * CHUNK_SIZEd) val xyAdd = Vector2d(pos.x * CHUNK_SIZEd, pos.y * CHUNK_SIZEd)
val seen = BooleanArray(CHUNK_SIZE * CHUNK_SIZE) val seen = BooleanArray(CHUNK_SIZE * CHUNK_SIZE)
for (y in 0 .. CHUNK_SIZE_FF) { /*for (y in 0 .. CHUNK_SIZE_FF) {
for (x 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) { if (!seen[x or (y shl CHUNK_SIZE_BITS)] && cells[x, y].foreground.material != null) {
val depthFirst = RectTileFlooderDepthFirst(cells, seen, x, y) val depthFirst = RectTileFlooderDepthFirst(this, seen, x, y)
val sizeFirst = RectTileFlooderSizeFirst(cells, seen, x, y) val sizeFirst = RectTileFlooderSizeFirst(this, seen, x, y)
val xSpanDepth = depthFirst.maxs.x - depthFirst.mins.x val xSpanDepth = depthFirst.maxs.x - depthFirst.mins.x
val ySpanDepth = depthFirst.maxs.y - depthFirst.mins.y val ySpanDepth = depthFirst.maxs.y - depthFirst.mins.y
val xSpanSize = sizeFirst.maxs.x - sizeFirst.mins.x val xSpanSize = sizeFirst.maxs.x - sizeFirst.mins.x
@ -320,7 +327,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
collisionCache.add(aabb + xyAdd) collisionCache.add(aabb + xyAdd)
} }
} }
} }*/
} }
/** /**
@ -338,8 +345,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
return world.chunkMap.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS) return world.chunkMap.randomLongFor(x or pos.x shl CHUNK_SIZE_BITS, y or pos.y shl CHUNK_SIZE_BITS)
} }
protected val entities = HashSet<Entity>() protected val entities = ReferenceOpenHashSet<Entity>()
val entitiesAccess: Set<Entity> = Collections.unmodifiableSet(entities)
protected abstract fun onEntityAdded(entity: Entity) protected abstract fun onEntityAdded(entity: Entity)
protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This) protected abstract fun onEntityTransferedToThis(entity: Entity, otherChunk: This)
@ -351,6 +357,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
throw IllegalArgumentException("Already having having entity $entity") throw IllegalArgumentException("Already having having entity $entity")
} }
changeset++
onEntityAdded(entity) onEntityAdded(entity)
} }
@ -366,6 +373,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
throw IllegalArgumentException("Already containing $entity") throw IllegalArgumentException("Already containing $entity")
} }
changeset++
onEntityTransferedToThis(entity, otherChunk as This) onEntityTransferedToThis(entity, otherChunk as This)
otherChunk.onEntityTransferedFromThis(entity, this as This) otherChunk.onEntityTransferedFromThis(entity, this as This)
@ -379,6 +387,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
throw IllegalArgumentException("Already not having entity $entity") throw IllegalArgumentException("Already not having entity $entity")
} }
changeset++
onEntityRemoved(entity) onEntityRemoved(entity)
} }

View File

@ -1,7 +1,7 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.math.roundByAbsoluteValue import ru.dbotthepony.kstarbound.math.roundByAbsoluteValue
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_BITS
import ru.dbotthepony.kvector.api.IStruct2d import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
@ -29,48 +29,39 @@ private fun circulate(value: Int, bounds: Int): Int {
data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> { data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
constructor(pos: IStruct2i) : this(pos.component1(), pos.component2()) constructor(pos: IStruct2i) : this(pos.component1(), pos.component2())
// bottom left corner fun tile(width: Int, height: Int): Vector2i {
val tileX: Int = x shl CHUNK_SIZE_BITS return Vector2i(x * width, y * height)
val tileY: Int = y shl CHUNK_SIZE_BITS }
val tile = Vector2i(tileX, tileY)
val top: ChunkPos val top: ChunkPos get() {
get() {
return ChunkPos(x, y + 1) return ChunkPos(x, y + 1)
} }
val bottom: ChunkPos val bottom: ChunkPos get() {
get() {
return ChunkPos(x, y - 1) return ChunkPos(x, y - 1)
} }
val left: ChunkPos val left: ChunkPos get() {
get() {
return ChunkPos(x - 1, y) return ChunkPos(x - 1, y)
} }
val topLeft: ChunkPos val topLeft: ChunkPos get() {
get() {
return ChunkPos(x - 1, y + 1) return ChunkPos(x - 1, y + 1)
} }
val topRight: ChunkPos val topRight: ChunkPos get() {
get() {
return ChunkPos(x + 1, y + 1) return ChunkPos(x + 1, y + 1)
} }
val bottomLeft: ChunkPos val bottomLeft: ChunkPos get() {
get() {
return ChunkPos(x - 1, y - 1) return ChunkPos(x - 1, y - 1)
} }
val bottomRight: ChunkPos val bottomRight: ChunkPos get() {
get() {
return ChunkPos(x + 1, y - 1) return ChunkPos(x + 1, y - 1)
} }
val right: ChunkPos val right: ChunkPos get() {
get() {
return ChunkPos(x + 1, y) return ChunkPos(x + 1, y)
} }
@ -114,54 +105,8 @@ data class ChunkPos(val x: Int, val y: Int) : IStruct2i, Comparable<ChunkPos> {
} }
companion object { companion object {
val ZERO = ChunkPos(0, 0)
fun toLong(x: Int, y: Int): Long { fun toLong(x: Int, y: Int): Long {
return x.toLong() or (y.toLong() shl 32) return x.toLong() or (y.toLong() shl 32)
} }
fun longFromPosition(x: Int, y: Int): Long {
return toLong(component(x), component(y))
}
fun fromPosition(input: IStruct2i): ChunkPos {
val (x, y) = input
return ChunkPos(component(x), component(y))
}
fun fromPosition(input: IStruct2d): ChunkPos {
val (x, y) = input
return fromPosition(x, y)
}
fun fromPosition(x: Int, y: Int): ChunkPos {
return ChunkPos(component(x), component(y))
}
fun fromPosition(x: Int, y: Int, xWrap: Int): ChunkPos {
return ChunkPos(circulate(component(x), xWrap), component(y))
}
fun fromPosition(x: Double, y: Double): ChunkPos {
return ChunkPos(
component(roundByAbsoluteValue(x)),
component(roundByAbsoluteValue(y))
)
}
fun fromPosition(x: Double, y: Double, xWrap: Int): ChunkPos {
return ChunkPos(
circulate(component(roundByAbsoluteValue(x)), xWrap),
component(roundByAbsoluteValue(y))
)
}
fun component(value: Int): Int {
if (value < 0) {
return -((-value) shr CHUNK_SIZE_BITS) - 1
}
return value shr CHUNK_SIZE_BITS
}
} }
} }

View File

@ -0,0 +1,114 @@
package ru.dbotthepony.kstarbound.world
abstract class CoordinateMapper {
protected fun positiveModulo(a: Int, b: Int): Int {
val result = a % b
return if (result < 0) result + b else result
}
protected fun positiveModulo(a: Double, b: Int): Double {
val result = a % b
return if (result < 0.0) result + b else result
}
protected fun positiveModulo(a: Float, b: Int): Float {
val result = a % b
return if (result < 0f) result + b else result
}
abstract fun cell(value: Int): Int
abstract fun cell(value: Double): Double
abstract fun cell(value: Float): Float
abstract fun chunk(value: Int): Int
abstract fun cellToChunk(value: Int): Int
fun cellToChunk(value: Float): Int = cellToChunk(value.toInt())
fun cellToChunk(value: Double): Int = cellToChunk(value.toInt())
abstract fun cellModulus(value: Int): Int
// inside world bounds
abstract fun inBoundsCell(value: Int): Boolean
abstract fun inBoundsChunk(value: Int): Boolean
// whenever provided index is legal (if world is wrapped around then provided index is legal outside world bounds)
open fun isValidCellIndex(value: Int): Boolean = inBoundsCell(value)
open fun isValidChunkIndex(value: Int): Boolean = inBoundsChunk(value)
object Infinite : CoordinateMapper() {
override fun cell(value: Int): Int = value
override fun cell(value: Double): Double = value
override fun cell(value: Float): Float = value
override fun chunk(value: Int): Int = value
override fun cellToChunk(value: Int): Int {
return value shr CHUNK_SIZE_BITS
}
override fun cellModulus(value: Int): Int {
return value and CHUNK_SIZE_MASK
}
override fun inBoundsCell(value: Int) = true
override fun inBoundsChunk(value: Int) = true
}
class Wrapper(private val cells: Int, private val chunkSize: Int) : CoordinateMapper() {
private val chunks = cells / chunkSize
override fun inBoundsCell(value: Int) = value in 0 until cells
override fun inBoundsChunk(value: Int) = value in 0 until chunks
override fun isValidCellIndex(value: Int) = true
override fun isValidChunkIndex(value: Int) = true
override fun cellToChunk(value: Int): Int {
return chunk(value / chunkSize)
}
override fun cellModulus(value: Int): Int {
return positiveModulo(value, chunkSize)
}
override fun cell(value: Int): Int = positiveModulo(value, cells)
override fun cell(value: Double): Double = positiveModulo(value, cells)
override fun cell(value: Float): Float = positiveModulo(value, cells)
override fun chunk(value: Int): Int = positiveModulo(value, chunks)
}
class Clamper(private val cells: Int, private val chunkSize: Int) : CoordinateMapper() {
private val chunks = cells / chunkSize
override fun inBoundsCell(value: Int): Boolean {
return value in 0 until cells
}
override fun cellModulus(value: Int): Int {
return positiveModulo(value, chunkSize)
}
override fun cellToChunk(value: Int): Int {
return chunk(value / chunkSize)
}
override fun inBoundsChunk(value: Int): Boolean {
return value in 0 until chunks
}
override fun cell(value: Int): Int {
return value.coerceIn(0, cells - 1)
}
override fun cell(value: Double): Double {
return value.coerceIn(0.0, cells - 1.0)
}
override fun cell(value: Float): Float {
return value.coerceIn(0f, cells - 1f)
}
override fun chunk(value: Int): Int {
return value.coerceIn(0, chunks - 1)
}
}
}

View File

@ -1,64 +0,0 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kvector.vector.Vector2i
fun ICellAccess.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator<Pair<Vector2i, IChunkCell>> {
return object : Iterator<Pair<Vector2i, IChunkCell>> {
private var x = fromX
private var y = fromY
override fun hasNext(): Boolean {
return x < toX && y < toY
}
override fun next(): Pair<Vector2i, IChunkCell> {
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 ITileAccess.iterateTiles(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator<Pair<Vector2i, ITileState>> {
return object : Iterator<Pair<Vector2i, ITileState>> {
private var x = fromX
private var y = fromY
override fun hasNext(): Boolean {
return x < toX && y < toY
}
override fun next(): Pair<Vector2i, ITileState> {
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
}
}
}

View File

@ -124,7 +124,7 @@ fun ICellAccess.castRayNaive(
val tilePos = Vector2i(x.roundToInt(), y.roundToInt()) val tilePos = Vector2i(x.roundToInt(), y.roundToInt())
if (tilePos != prev) { if (tilePos != prev) {
val tile = getCell(tilePos) val tile = getCell(tilePos) ?: break
when (filter.test(tile, t, tilePos)) { when (filter.test(tile, t, tilePos)) {
RayFilterResult.HIT -> { RayFilterResult.HIT -> {

View File

@ -13,9 +13,6 @@ import ru.dbotthepony.kbox2d.dynamics.B2World
import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.util.Timer import ru.dbotthepony.kstarbound.util.Timer
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_MASK
import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.api.TileView
@ -28,6 +25,14 @@ import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.util2d.AABBi import ru.dbotthepony.kvector.util2d.AABBi
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
const val CHUNK_SIZE_BITS = 5
const val CHUNK_SIZE_MASK = 1 or 2 or 4 or 8 or 16
const val CHUNK_SIZE = 1 shl CHUNK_SIZE_BITS // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
const val CHUNK_SIZEd = CHUNK_SIZE.toDouble()
abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>( abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, ChunkType>>(
val seed: Long, val seed: Long,
@ -35,164 +40,73 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val loopX: Boolean, val loopX: Boolean,
val loopY: Boolean val loopY: Boolean
) { ) {
abstract class AbstractCoordinatesWrapper { private fun determineChunkSize(cells: Int): Int {
abstract fun cell(value: Int): Int for (i in 32 downTo 1) {
abstract fun cell(value: Double): Double if (cells % i == 0) {
abstract fun cell(value: Float): Float return i
abstract fun chunk(value: Int): Int
abstract fun cellOffset(chunk: Int): Int
abstract fun inBounds(value: Int): Boolean
abstract fun inBoundsChunk(value: Int): Boolean
protected fun positiveModulo(a: Int, b: Int): Int {
val result = a % b
return if (result < 0) result + b else result
}
protected fun positiveModulo(a: Double, b: Int): Double {
val result = a % b
return if (result < 0.0) result + b else result
}
protected fun positiveModulo(a: Float, b: Int): Float {
val result = a % b
return if (result < 0f) result + b else result
}
}
object PassthroughWrapper : AbstractCoordinatesWrapper() {
override fun cell(value: Int): Int = value
override fun cell(value: Double): Double = value
override fun cell(value: Float): Float = value
override fun chunk(value: Int): Int = value
override fun cellOffset(chunk: Int): Int = 0
override fun inBounds(value: Int) = true
override fun inBoundsChunk(value: Int) = true
}
class CoordinatesWrapper(private val cell: Int, private val chunk: Int) : AbstractCoordinatesWrapper() {
override fun inBounds(value: Int): Boolean {
return value in 0 until cell
}
override fun inBoundsChunk(value: Int): Boolean {
return value in 0 until chunk
}
private val misalignedTiles = cell and CHUNK_SIZE_MASK
private val misalignedTiles2 = misalignedTiles - CHUNK_SIZE
override fun cellOffset(chunk: Int): Int {
if (misalignedTiles == 0) {
return 0
} else if (chunk >= 0) {
return (chunk / this.chunk) * misalignedTiles2
} else {
return ((chunk + 1) / this.chunk - 1) * misalignedTiles2
} }
} }
override fun cell(value: Int): Int { throw RuntimeException("unreachable code")
return positiveModulo(value, cell)
}
override fun cell(value: Double): Double {
return positiveModulo(value, cell)
}
override fun cell(value: Float): Float {
return positiveModulo(value, cell)
}
override fun chunk(value: Int): Int {
return positiveModulo(value, chunk)
}
} }
class CoordinatesClamper(private val cell: Int, private val chunk: Int) : AbstractCoordinatesWrapper() { val chunkWidth = size?.x?.let(::determineChunkSize) ?: CHUNK_SIZE
override fun inBounds(value: Int): Boolean { val chunkHeight = size?.y?.let(::determineChunkSize) ?: CHUNK_SIZE
return value in 0 until cell
}
override fun cellOffset(chunk: Int): Int {
return 0
}
override fun inBoundsChunk(value: Int): Boolean {
return value in 0 until chunk
}
override fun cell(value: Int): Int {
return value.coerceIn(0, cell - 1)
}
override fun cell(value: Double): Double {
return value.coerceIn(0.0, cell - 1.0)
}
override fun cell(value: Float): Float {
return value.coerceIn(0f, cell - 1f)
}
override fun chunk(value: Int): Int {
return value.coerceIn(0, chunk - 1)
}
}
abstract inner class ChunkMap : ICellAccess { abstract inner class ChunkMap : ICellAccess {
abstract val x: AbstractCoordinatesWrapper abstract val x: CoordinateMapper
abstract val y: AbstractCoordinatesWrapper abstract val y: CoordinateMapper
fun inBounds(x: Int, y: Int) = this.x.inBounds(x) && this.y.inBounds(y)
fun inBounds(value: IStruct2i) = this.x.inBounds(value.component1()) && this.y.inBounds(value.component2())
override fun randomLongFor(x: Int, y: Int): Long {
return super.randomLongFor(x, y) xor seed
}
val background = TileView.Background(this) val background = TileView.Background(this)
val foreground = TileView.Foreground(this) val foreground = TileView.Foreground(this)
fun inBounds(x: Int, y: Int) = this.x.inBoundsCell(x) && this.y.inBoundsCell(y)
fun inBounds(value: IStruct2i) = this.x.inBoundsCell(value.component1()) && this.y.inBoundsCell(value.component2())
fun cellToChunk(x: Int, y: Int) = ChunkPos(this.x.cellToChunk(x), this.y.cellToChunk(y))
fun cellToChunk(x: Double, y: Double) = ChunkPos(this.x.cellToChunk(x.toInt()), this.y.cellToChunk(y.toInt()))
fun cellToChunk(value: IStruct2i) = cellToChunk(value.component1(), value.component2())
fun cellToChunk(value: IStruct2d) = cellToChunk(value.component1(), value.component2())
final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed
abstract operator fun get(x: Int, y: Int): ChunkType? abstract operator fun get(x: Int, y: Int): ChunkType?
operator fun get(pos: ChunkPos) = get(pos.x, pos.y) operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
open fun chunkFromCell(x: Int, y: Int): ChunkType? { abstract fun promote(self: ChunkType)
return get(ChunkPos.component(x), ChunkPos.component(y))
protected val queue = ReferenceQueue<ChunkType>()
abstract fun purge()
protected inner class Ref(chunk: ChunkType) : WeakReference<ChunkType>(chunk, queue) {
val pos = chunk.pos
} }
fun chunkFromCell(pos: IStruct2i) = chunkFromCell(pos.component1(), pos.component2())
override fun getCell(x: Int, y: Int): IChunkCell {
return chunkFromCell(x, y)?.getCell(x and CHUNK_SIZE_MASK, y and CHUNK_SIZE_MASK) ?: IChunkCell.Companion
}
abstract operator fun set(x: Int, y: Int, chunk: ChunkType)
abstract fun remove(x: Int, y: Int) abstract fun remove(x: Int, y: Int)
fun cellToGrid(x: Int, y: Int) = ChunkPos.fromPosition(this.x.cell(x), this.y.cell(y)) override fun getCell(x: Int, y: Int): IChunkCell? {
fun cellToGrid(x: Double, y: Double) = ChunkPos.fromPosition(this.x.cell(x), this.y.cell(y)) if (!this.x.isValidCellIndex(x) || !this.y.isValidCellIndex(y)) return null
fun cellToGrid(position: IStruct2i) = cellToGrid(position.component1(), position.component2()) val ix = this.x.cell(x)
fun cellToGrid(position: IStruct2d) = cellToGrid(position.component1(), position.component2()) val iy = this.y.cell(y)
return get(this.x.cellToChunk(ix), this.y.cellToChunk(iy))?.getCell(this.x.cellModulus(ix), this.y.cellModulus(iy))
}
fun computeIfAbsent(pos: ChunkPos) = computeIfAbsent(pos.x, pos.y) override fun getCellDirect(x: Int, y: Int): IChunkCell? {
if (!this.x.inBoundsCell(x) || !this.y.inBoundsCell(y)) return null
return getCell(x, y)
}
fun computeIfAbsent(x: Int, y: Int): ChunkType { protected fun create(x: Int, y: Int): ChunkType {
val existing = get(x, y) purge()
val pos = ChunkPos(x, y)
if (existing != null)
return existing
val pos = ChunkPos(this.x.chunk(x), this.y.chunk(y))
val chunk = chunkFactory(pos) val chunk = chunkFactory(pos)
val orphanedInThisChunk = ArrayList<Entity>() val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) { for (ent in orphanedEntities) {
val (ex, ey) = ent.position val (ex, ey) = ent.position
if (ChunkPos.fromPosition(this.x.cell(ex), this.y.cell(ey)) == pos) { if (this.x.cellToChunk(ex) == x && this.y.cellToChunk(ey) == y) {
orphanedInThisChunk.add(ent) orphanedInThisChunk.add(ent)
} }
} }
@ -201,65 +115,124 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
ent.chunk = chunk ent.chunk = chunk
} }
set(x, y, chunk)
return chunk return chunk
} }
} }
// infinite chunk map is around 30% slower than rectangular one // infinite chunk map is around 30% slower than rectangular one
inner class InfiniteChunkMap : ChunkMap() { inner class InfiniteChunkMap : ChunkMap() {
private val map = Long2ObjectOpenHashMap<ChunkType>() private val map = Long2ObjectOpenHashMap<Any>()
override val x = CoordinateMapper.Infinite
override val y = CoordinateMapper.Infinite
override fun get(x: Int, y: Int): ChunkType? { override fun get(x: Int, y: Int): ChunkType {
return map[ChunkPos.toLong(x, y)] return map[ChunkPos.toLong(x, y)]?.let {
if (it is World<*, *>.ChunkMap.Ref) {
it.get() as ChunkType?
} else {
it as ChunkType?
}
} ?: create(x, y).also {
map[ChunkPos.toLong(x, y)] = Ref(it)
}
} }
override fun set(x: Int, y: Int, chunk: ChunkType) { override fun purge() {
map[ChunkPos.toLong(x, y)] = chunk var next = queue.poll() as World<*, *>.ChunkMap.Ref?
while (next != null) {
val get = map[ChunkPos.toLong(next.pos.x, next.pos.y)]
if (get === next) {
map.remove(ChunkPos.toLong(next.pos.x, next.pos.y))
}
next = queue.poll() as World<*, *>.ChunkMap.Ref?
}
}
override fun promote(self: ChunkType) {
val (x, y) = self.pos
val ref = map[ChunkPos.toLong(x, y)]
if (ref !is World<*, *>.ChunkMap.Ref) {
throw IllegalStateException("Tried to promote chunk from weak to strong storage at $x, $y; but there is $ref")
}
if (!(ref as World<*, ChunkType>.ChunkMap.Ref).refersTo(self)) {
throw IllegalStateException("Tried to promote chunk from weak to strong storage at $x, $y; but it doesn't refer to valid object (self: $self, referent: ${ref.get()})")
}
map[ChunkPos.toLong(x, y)] = self
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
map.remove(ChunkPos.toLong(x, y)) val ref = map.remove(ChunkPos.toLong(x, y))
}
override val x = PassthroughWrapper if (ref is World<*, *>.ChunkMap.Ref) {
override val y = PassthroughWrapper ref.clear()
}
}
} }
inner class RectChunkMap : ChunkMap() { inner class RectChunkMap : ChunkMap() {
val width = size!!.x val width = size!!.x
val height = size!!.y val height = size!!.y
val widthInChunks = if (width and CHUNK_SIZE_MASK == 0) width / CHUNK_SIZE else width / CHUNK_SIZE + 1 override val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(width, chunkWidth) else CoordinateMapper.Clamper(width, chunkWidth)
val heightInChunks = if (height and CHUNK_SIZE_MASK == 0) height / CHUNK_SIZE else height / CHUNK_SIZE + 1 override val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(height, chunkHeight) else CoordinateMapper.Clamper(height, chunkHeight)
override val x: AbstractCoordinatesWrapper = if (loopX) CoordinatesWrapper(width, widthInChunks) else CoordinatesClamper(width, widthInChunks) private val map = Object2DArray.nulls<Any>(width / chunkWidth, height / chunkHeight)
override val y: AbstractCoordinatesWrapper = if (loopY) CoordinatesWrapper(height, heightInChunks) else CoordinatesClamper(height, heightInChunks)
private val map = Object2DArray.nulls<ChunkType>(widthInChunks, heightInChunks)
override fun get(x: Int, y: Int): ChunkType? { override fun get(x: Int, y: Int): ChunkType? {
return map[this.x.chunk(x), this.y.chunk(y)] if (!this.x.isValidChunkIndex(x) || !this.y.isValidChunkIndex(y)) return null
val ix = this.x.chunk(x)
val iy = this.x.chunk(y)
return map[ix, iy]?.let {
if (it is World<*, *>.ChunkMap.Ref) {
it.get() as ChunkType?
} else {
it as ChunkType?
}
} ?: create(ix, iy).also {
map[ix, iy] = Ref(it)
}
} }
override fun chunkFromCell(x: Int, y: Int): ChunkType? { override fun purge() {
val ix = this.x.cell(x) while (queue.poll() != null) {}
val iy = this.y.cell(y)
return get(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS)
} }
override fun getCell(x: Int, y: Int): IChunkCell { override fun promote(self: ChunkType) {
val ix = this.x.cell(x) val (x, y) = self.pos
val iy = this.y.cell(y)
return get(ix ushr CHUNK_SIZE_BITS, iy ushr CHUNK_SIZE_BITS)?.getCell(ix and CHUNK_SIZE_MASK, iy and CHUNK_SIZE_MASK) ?: IChunkCell.Companion
}
override fun set(x: Int, y: Int, chunk: ChunkType) { val ref = map[x, y]
map[this.x.chunk(x), this.y.chunk(y)] = chunk
if (ref !is World<*, *>.ChunkMap.Ref) {
throw IllegalStateException("Tried to promote chunk from weak to strong storage at $x, $y; but there is $ref")
}
if (!(ref as World<*, ChunkType>.ChunkMap.Ref).refersTo(self)) {
throw IllegalStateException("Tried to promote chunk from weak to strong storage at $x, $y; but it doesn't refer to valid object (self: $self, referent: ${ref.get()})")
}
map[x, y] = self
} }
override fun remove(x: Int, y: Int) { override fun remove(x: Int, y: Int) {
map[this.x.chunk(x), this.y.chunk(y)] = null if (!this.x.isValidChunkIndex(x) || !this.y.isValidChunkIndex(y)) return
val ix = this.x.chunk(x)
val iy = this.x.chunk(y)
val old = map[ix, iy]
if (old is World<*, *>.ChunkMap.Ref) {
old.clear()
}
map[ix, iy] = null
} }
} }
@ -380,7 +353,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
dirtyPhysicsChunks.clear() dirtyPhysicsChunks.clear()
physics.step(delta, 6, 4) //physics.step(delta, 6, 4)
timer += delta timer += delta
@ -432,29 +405,12 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
protected abstract fun chunkFactory(pos: ChunkPos): ChunkType protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
/**
* Возвращает все чанки, которые пересекаются с заданным [boundingBox]
*/
fun collectWithPosition(boundingBox: AABBi): List<Pair<ChunkPos, ChunkType>> {
val output = ArrayList<Pair<ChunkPos, ChunkType>>()
for (pos in boundingBox.chunkPositions) {
val chunk = chunkMap[pos]
if (chunk != null && (loopX || chunkMap.x.inBoundsChunk(pos.x)) && (loopY || chunkMap.y.inBoundsChunk(pos.y))) {
output.add(pos to chunk)
}
}
return output
}
fun testSpace(aabb: AABB, predicate: Predicate<IChunkCell> = Predicate { it.foreground.material != null }): Boolean { fun testSpace(aabb: AABB, predicate: Predicate<IChunkCell> = Predicate { it.foreground.material != null }): Boolean {
val tiles = aabb.encasingIntAABB() val tiles = aabb.encasingIntAABB()
for (x in tiles.mins.x .. tiles.maxs.x) { for (x in tiles.mins.x .. tiles.maxs.x) {
for (y in tiles.mins.y .. tiles.maxs.y) { for (y in tiles.mins.y .. tiles.maxs.y) {
if (predicate.test(chunkMap.getCell(x, y))) { if (predicate.test(chunkMap.getCell(x, y) ?: continue)) {
return true return true
} }
} }
@ -482,13 +438,13 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
var newIntensity: Int var newIntensity: Int
if (tile.foreground.material?.renderParameters?.lightTransparent == false) { if (tile?.foreground?.material?.renderParameters?.lightTransparent == false) {
newIntensity = thisIntensity - lightBlockerStrength newIntensity = thisIntensity - lightBlockerStrength
} else { } else {
newIntensity = thisIntensity - 1 newIntensity = thisIntensity - 1
} }
if (tile.foreground.material != null) if (tile?.foreground?.material != null)
newIntensity = 0 newIntensity = 0
lightmap[posX, posY] = newIntensity.coerceAtLeast(0) lightmap[posX, posY] = newIntensity.coerceAtLeast(0)

View File

@ -1,8 +0,0 @@
package ru.dbotthepony.kstarbound.world.api
const val CHUNK_SIZE_BITS = 5
const val CHUNK_SIZE_MASK = 1 or 2 or 4 or 8 or 16
const val CHUNK_SIZE = 1 shl CHUNK_SIZE_BITS // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
const val CHUNK_SIZEf = CHUNK_SIZE.toFloat()
const val CHUNK_SIZEd = CHUNK_SIZE.toDouble()

View File

@ -1,14 +1,31 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kvector.api.IStruct2i import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
interface ICellAccess { interface ICellAccess {
// relative /**
fun getCell(x: Int, y: Int): IChunkCell * non-null - valid cell (maybe wrapped around)
* null - invalid cell (outside world bounds)
*/
fun getCell(x: Int, y: Int): IChunkCell?
fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2()) fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2())
/**
* non-null - valid cell and not wrapped around
* null - invalid cell (outside world bounds)
*/
fun getCellDirect(x: Int, y: Int): IChunkCell? {
val cell = getCell(x, y)
if (cell == null || cell.x != x || cell.y != y)
return null
return cell
}
fun getCellDirect(pos: IStruct2i) = getCellDirect(pos.component1(), pos.component2())
/** /**
* Возвращает псевдослучайное Long для заданной позиции * Возвращает псевдослучайное Long для заданной позиции
* *
@ -38,9 +55,8 @@ interface ICellAccess {
class OffsetCellAccess(private val parent: ICellAccess, private val x: Int, private val y: Int) : ICellAccess { class OffsetCellAccess(private val parent: ICellAccess, private val x: Int, private val y: Int) : ICellAccess {
constructor(parent: ICellAccess, offset: IStruct2i) : this(parent, offset.component1(), offset.component2()) constructor(parent: ICellAccess, offset: IStruct2i) : this(parent, offset.component1(), offset.component2())
constructor(parent: ICellAccess, offset: ChunkPos) : this(parent, offset.tile)
override fun getCell(x: Int, y: Int): IChunkCell { override fun getCell(x: Int, y: Int): IChunkCell? {
return parent.getCell(x + this.x, y + this.y) return parent.getCell(x + this.x, y + this.y)
} }

View File

@ -7,6 +7,10 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import java.io.DataInputStream import java.io.DataInputStream
interface IChunkCell { interface IChunkCell {
// absolute in-world coordinates
val x: Int
val y: Int
val foreground: ITileState val foreground: ITileState
val background: ITileState val background: ITileState
val liquid: ILiquidState val liquid: ILiquidState
@ -16,8 +20,6 @@ interface IChunkCell {
var envBiome: Int var envBiome: Int
var isIndestructible: Boolean var isIndestructible: Boolean
val isEmpty: Boolean
fun read( fun read(
materialAccess: (Int) -> RegistryObject<TileDefinition>?, materialAccess: (Int) -> RegistryObject<TileDefinition>?,
modifierAccess: (Int) -> RegistryObject<MaterialModifier>?, modifierAccess: (Int) -> RegistryObject<MaterialModifier>?,
@ -28,36 +30,22 @@ interface IChunkCell {
background.read(materialAccess, modifierAccess, stream) background.read(materialAccess, modifierAccess, stream)
liquid.read(liquidAccess, stream) liquid.read(liquidAccess, stream)
stream.skipBytes(1) // collisionMap stream.skipNBytes(1) // collisionMap
dungeonId = stream.readUnsignedShort() dungeonId = stream.readUnsignedShort()
biome = stream.readUnsignedByte() biome = stream.readUnsignedByte()
envBiome = stream.readUnsignedByte() envBiome = stream.readUnsignedByte()
isIndestructible = stream.readBoolean() isIndestructible = stream.readBoolean()
stream.skipBytes(1) // unknown stream.skipNBytes(1) // unknown
} }
companion object : IChunkCell { companion object {
override val foreground: ITileState fun skip(stream: DataInputStream) {
get() = ITileState ITileState.skip(stream)
override val background: ITileState ITileState.skip(stream)
get() = ITileState ILiquidState.skip(stream)
override val liquid: ILiquidState stream.skipNBytes(1 + 2 + 1 + 1 + 1 + 1)
get() = ILiquidState }
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
} }
} }

View File

@ -10,8 +10,6 @@ interface ILiquidState {
var pressure: Float var pressure: Float
var isInfinite: Boolean var isInfinite: Boolean
val isEmpty: Boolean
fun read( fun read(
liquidAccess: (Int) -> RegistryObject<LiquidDefinition>?, liquidAccess: (Int) -> RegistryObject<LiquidDefinition>?,
stream: DataInputStream stream: DataInputStream
@ -22,20 +20,9 @@ interface ILiquidState {
isInfinite = stream.readBoolean() isInfinite = stream.readBoolean()
} }
companion object : ILiquidState { companion object {
override var def: LiquidDefinition? fun skip(stream: DataInputStream) {
get() = null stream.skipNBytes(1 + 4 + 4 + 1)
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
} }
} }

View File

@ -5,20 +5,30 @@ import ru.dbotthepony.kvector.api.IStruct2i
// for getting tiles directly, avoiding manual layer specification // for getting tiles directly, avoiding manual layer specification
interface ITileAccess : ICellAccess { interface ITileAccess : ICellAccess {
// relative // relative
fun getTile(x: Int, y: Int): ITileState fun getTile(x: Int, y: Int): ITileState?
fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2()) fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2())
fun getTileDirect(x: Int, y: Int): ITileState?
fun getTileDirect(pos: IStruct2i) = getTile(pos.component1(), pos.component2())
} }
sealed class TileView(parent: ICellAccess) : ITileAccess, ICellAccess by parent { sealed class TileView(parent: ICellAccess) : ITileAccess, ICellAccess by parent {
class Foreground(parent: ICellAccess) : TileView(parent) { class Foreground(parent: ICellAccess) : TileView(parent) {
override fun getTile(x: Int, y: Int): ITileState { override fun getTile(x: Int, y: Int): ITileState? {
return getCell(x, y).foreground return getCell(x, y)?.foreground
}
override fun getTileDirect(x: Int, y: Int): ITileState? {
return getCellDirect(x, y)?.foreground
} }
} }
class Background(parent: ICellAccess) : TileView(parent) { class Background(parent: ICellAccess) : TileView(parent) {
override fun getTile(x: Int, y: Int): ITileState { override fun getTile(x: Int, y: Int): ITileState? {
return getCell(x, y).background return getCell(x, y)?.background
}
override fun getTileDirect(x: Int, y: Int): ITileState? {
return getCellDirect(x, y)?.background
} }
} }
} }

View File

@ -12,8 +12,6 @@ interface ITileState {
var hueShift: Float var hueShift: Float
var modifierHueShift: Float var modifierHueShift: Float
val isEmpty: Boolean
fun setHueShift(value: Int) { fun setHueShift(value: Int) {
if (value < 0) { if (value < 0) {
hueShift = 0f hueShift = 0f
@ -46,23 +44,9 @@ interface ITileState {
setModHueShift(stream.read()) setModHueShift(stream.read())
} }
companion object : ITileState { companion object {
override var material: TileDefinition? fun skip(stream: DataInputStream) {
get() = null stream.skipNBytes(2 + 1 + 1 + 2 + 1)
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
} }
} }

View File

@ -94,7 +94,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
return return
} }
val chunkPos = world.chunkMap.cellToGrid(position) val chunkPos = world.chunkMap.cellToChunk(position)
if (value != null && chunkPos != value.pos) { if (value != null && chunkPos != value.pos) {
throw IllegalStateException("Set proper position before setting chunk this Entity belongs to (expected chunk $chunkPos, got chunk ${value.pos})") throw IllegalStateException("Set proper position before setting chunk this Entity belongs to (expected chunk $chunkPos, got chunk ${value.pos})")
@ -125,8 +125,8 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
movement.notifyPositionChanged() movement.notifyPositionChanged()
if (isSpawned && !isRemoved) { if (isSpawned && !isRemoved) {
val oldChunkPos = world.chunkMap.cellToGrid(old) val oldChunkPos = world.chunkMap.cellToChunk(old)
val newChunkPos = world.chunkMap.cellToGrid(field) val newChunkPos = world.chunkMap.cellToChunk(field)
if (oldChunkPos != newChunkPos) { if (oldChunkPos != newChunkPos) {
chunk = world.chunkMap[newChunkPos] chunk = world.chunkMap[newChunkPos]
@ -154,7 +154,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
isSpawned = true isSpawned = true
world.entities.add(this) world.entities.add(this)
chunk = world.chunkMap[world.chunkMap.cellToGrid(position)] chunk = world.chunkMap[world.chunkMap.cellToChunk(position)]
if (chunk == null) { if (chunk == null) {
world.orphanedEntities.add(this) world.orphanedEntities.add(this)

View File

@ -1,13 +1,14 @@
package ru.dbotthepony.kstarbound.world.phys package ru.dbotthepony.kstarbound.world.phys
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_BITS
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
class RectTileFlooderDepthFirst( class RectTileFlooderDepthFirst(
private val tiles: Object2DArray<out IChunkCell>, private val tiles: ICellAccess,
private val seen: BooleanArray, private val seen: BooleanArray,
rootx: Int, rootx: Int,
rooty: Int rooty: Int
@ -24,7 +25,7 @@ class RectTileFlooderDepthFirst(
return false return false
} }
return !seen[x or (y shl CHUNK_SIZE_BITS)] && tiles[x, y].foreground.material != null return !seen[x or (y shl CHUNK_SIZE_BITS)] && tiles.getCell(x, y)?.foreground?.material != null
} }
init { init {

View File

@ -1,13 +1,14 @@
package ru.dbotthepony.kstarbound.world.phys package ru.dbotthepony.kstarbound.world.phys
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_BITS
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kvector.arrays.Object2DArray import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
class RectTileFlooderSizeFirst( class RectTileFlooderSizeFirst(
private val tiles: Object2DArray<out IChunkCell>, private val tiles: ICellAccess,
private val seen: BooleanArray, private val seen: BooleanArray,
private val rootx: Int, private val rootx: Int,
private val rooty: Int private val rooty: Int
@ -24,7 +25,7 @@ class RectTileFlooderSizeFirst(
return false return false
} }
return !seen[x or (y shl CHUNK_SIZE_BITS)] && tiles[x, y].foreground.material != null return !seen[x or (y shl CHUNK_SIZE_BITS)] && tiles.getCell(x, y)?.foreground?.material != null
} }
private var widthPositive = 0 private var widthPositive = 0

View File

@ -3,8 +3,8 @@ package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.math.*
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
object MathTests { object MathTests {