Chunk map, circular worlds second attempt

This commit is contained in:
DBotThePony 2023-09-03 23:08:10 +07:00
parent e436864e12
commit 0657ee8ef7
Signed by: DBot
GPG Key ID: DCC23B5715498507
21 changed files with 623 additions and 575 deletions

View File

@ -65,7 +65,8 @@ fun main() {
var parse = 0L var parse = 0L
//for (chunkX in 17 .. 18) { //for (chunkX in 17 .. 18) {
for (chunkX in 14 .. 24) { //for (chunkX in 14 .. 24) {
for (chunkX in 0 .. 100) {
// for (chunkY in 21 .. 21) { // for (chunkY in 21 .. 21) {
for (chunkY in 18 .. 24) { for (chunkY in 18 .. 24) {
var t = System.currentTimeMillis() var t = System.currentTimeMillis()
@ -73,7 +74,7 @@ fun main() {
find += System.currentTimeMillis() - t find += System.currentTimeMillis() - t
if (data != null) { if (data != null) {
val chunk = client.world!!.computeIfAbsent(ChunkPos(chunkX, chunkY)) val chunk = client.world!!.chunkMap.computeIfAbsent(ChunkPos(chunkX, chunkY))
val inflater = Inflater() val inflater = Inflater()
inflater.setInput(data) inflater.setInput(data)
@ -105,12 +106,15 @@ fun main() {
val item = starbound.items.values.random() val item = starbound.items.values.random()
val rand = java.util.Random() val rand = java.util.Random()
for (i in 0 .. 10) { client.world!!.physics.gravity = Vector2d.ZERO
for (i in 0 .. 0) {
val item = ItemEntity(client.world!!, item.value) val item = ItemEntity(client.world!!, item.value)
item.position = Vector2d(600.0 + 16.0 + i, 721.0 + 48.0) item.position = Vector2d(7.0 + i, 685.0)
item.spawn() item.spawn()
item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0))
item.movement.applyVelocity(Vector2d(-1.0, 0.0))
} }
// println(Starbound.statusEffects["firecharge"]) // println(Starbound.statusEffects["firecharge"])
@ -144,13 +148,14 @@ 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(578f, 695f) client.camera.pos = Vector2f(7f, 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("${client.camera.pos} ${client.settings.zoom}", y = 140f, scale = 0.25f)
client.gl.font.render("${ChunkPos.fromTilePosition(client.camera.pos.toDoubleVector())}", y = 160f, scale = 0.25f) client.gl.font.render("Camera: ${ChunkPos.fromPosition(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

@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.world.*
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEf import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEf
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.ITileChunk import ru.dbotthepony.kstarbound.world.api.ITileChunk
import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.arrays.Matrix4fStack import ru.dbotthepony.kvector.arrays.Matrix4fStack
@ -42,12 +43,13 @@ const val Z_LEVEL_LIQUID = 10000
class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable { class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, ClientChunk>(world, pos), Closeable {
val state: GLStateTracker get() = world.client.gl val state: GLStateTracker get() = world.client.gl
private inner class TileLayerRenderer(private val layerChangeset: IntSupplier, private val view: () -> ITileChunk, private val isBackground: Boolean) : AutoCloseable { private inner class TileLayerRenderer(private val view: () -> ITileChunk, 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>>()
private var changeset = -1 var isDirty = true
fun bake() { fun bake() {
isDirty = false
val view = view() val view = view()
if (state.isSameThread()) { if (state.isSameThread()) {
@ -60,7 +62,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
layers.clear() layers.clear()
for ((pos, tile) in view.iterate()) { for ((pos, tile) in view.iterateTiles()) {
val material = tile.material val material = tile.material
if (material != null) { if (material != null) {
@ -78,7 +80,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
fun loadRenderers() { fun loadRenderers() {
val view = view() val view = view()
for ((_, tile) in view.iterate()) { for ((_, tile) in view.iterateTiles()) {
val material = tile.material val material = tile.material
val modifier = tile.modifier val modifier = tile.modifier
@ -111,9 +113,8 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
} }
fun autoBake() { fun autoBake() {
if (changeset != layerChangeset.asInt) { if (isDirty) {
this.bake() bake()
changeset = layerChangeset.asInt
} }
} }
@ -153,8 +154,28 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
val debugCollisions get() = world.client.settings.debugCollisions val debugCollisions get() = world.client.settings.debugCollisions
val posVector2d = Vector2d(x = pos.x * CHUNK_SIZEd, y = pos.y * CHUNK_SIZEd) val posVector2d = Vector2d(x = pos.x * CHUNK_SIZEd, y = pos.y * CHUNK_SIZEd)
private val foregroundRenderer = TileLayerRenderer(::foregroundChangeset, { world.getView(pos).foregroundView }, isBackground = false) private val foregroundRenderer = TileLayerRenderer({ world.getView(pos).foregroundView }, isBackground = false)
private val backgroundRenderer = TileLayerRenderer(::backgroundChangeset, { world.getView(pos).backgroundView }, isBackground = true) private val backgroundRenderer = TileLayerRenderer({ world.getView(pos).backgroundView }, isBackground = true)
override fun foregroundChanges() {
super.foregroundChanges()
foregroundRenderer.isDirty = true
forEachNeighbour {
it.foregroundRenderer.isDirty = true
}
}
override fun backgroundChanges() {
super.backgroundChanges()
backgroundRenderer.isDirty = true
forEachNeighbour {
it.backgroundRenderer.isDirty = true
}
}
/** /**
* Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее) * Принудительно подгружает в GLStateTracker все необходимые рендереры (ибо им нужны текстуры и прочее)

View File

@ -5,12 +5,15 @@ import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB
import ru.dbotthepony.kstarbound.world.* import ru.dbotthepony.kstarbound.world.*
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.Vector2i
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
seed: Long, seed: Long,
widthInChunks: Int, size: Vector2i? = null,
) : World<ClientWorld, ClientChunk>(seed, widthInChunks) { loopX: Boolean = false,
loopY: Boolean = false
) : World<ClientWorld, ClientChunk>(seed, size, loopX, loopY) {
init { init {
physics.debugDraw = client.gl.box2dRenderer physics.debugDraw = client.gl.box2dRenderer
} }
@ -195,8 +198,6 @@ class ClientWorld(
//frame.close() //frame.close()
//texture.close() //texture.close()
physics.debugDraw()
/*for (renderer in determineRenderers) { /*for (renderer in determineRenderers) {
renderer.renderDebug() renderer.renderDebug()
}*/ }*/

View File

@ -26,6 +26,7 @@ import ru.dbotthepony.kvector.util2d.AABB
import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.RGBAColor
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 ru.dbotthepony.kvector.vector.Vector3f import ru.dbotthepony.kvector.vector.Vector3f
import java.io.Closeable import java.io.Closeable
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -235,7 +236,7 @@ class StarboundClient(val starbound: Starbound) : Closeable {
lightRenderer.resizeFramebuffer(viewportWidth, viewportHeight) lightRenderer.resizeFramebuffer(viewportWidth, viewportHeight)
} }
var world: ClientWorld? = ClientWorld(this, 0L, 0) var world: ClientWorld? = ClientWorld(this, 0L, Vector2i(3000, 2000), true)
init { init {
putDebugLog("Initialized OpenGL context") putDebugLog("Initialized OpenGL context")
@ -345,6 +346,8 @@ class StarboundClient(val starbound: Starbound) : Closeable {
layers.render(gl.matrixStack) layers.render(gl.matrixStack)
world.physics.debugDraw()
for (lambda in onPostDrawWorld) { for (lambda in onPostDrawWorld) {
lambda.invoke() lambda.invoke()
} }

View File

@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.client.gl.shader.GLTileProgram
import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList import ru.dbotthepony.kstarbound.client.gl.vertex.GLAttributeList
import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.client.gl.vertex.*
import ru.dbotthepony.kstarbound.defs.tile.* import ru.dbotthepony.kstarbound.defs.tile.*
import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.ITileChunk import ru.dbotthepony.kstarbound.world.api.ITileChunk
import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor

View File

@ -6,7 +6,7 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory import ru.dbotthepony.kstarbound.io.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.util.WriteOnce
import ru.dbotthepony.kstarbound.world.api.ITileChunk import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
@ -38,7 +38,7 @@ data class RenderRuleList(
val matchHue: Boolean = false, val matchHue: Boolean = false,
val inverse: Boolean = false, val inverse: Boolean = false,
) { ) {
private fun doTest(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { private fun doTest(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
return when (type) { return when (type) {
"EqualsSelf" -> equalityTester.test(getter.getTile(thisPos), getter.getTile(thisPos + offsetPos)) "EqualsSelf" -> equalityTester.test(getter.getTile(thisPos), getter.getTile(thisPos + offsetPos))
"Connects" -> getter.getTile(thisPos + offsetPos).material != null "Connects" -> getter.getTile(thisPos + offsetPos).material != null
@ -53,7 +53,7 @@ data class RenderRuleList(
} }
} }
fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean { fun test(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offsetPos: Vector2i): Boolean {
if (inverse) { if (inverse) {
return !doTest(getter, equalityTester, thisPos, offsetPos) return !doTest(getter, equalityTester, thisPos, offsetPos)
} }
@ -67,7 +67,7 @@ data class RenderRuleList(
} }
} }
fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean { fun test(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i, offset: Vector2i): Boolean {
when (join) { when (join) {
Combination.ALL -> { Combination.ALL -> {
for (entry in entries) { for (entry in entries) {
@ -120,7 +120,7 @@ data class RenderMatch(
) { ) {
var rule by WriteOnce<RenderRuleList>() var rule by WriteOnce<RenderRuleList>()
fun test(getter: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { fun test(getter: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean {
return rule.test(getter, equalityTester, thisPos, offset) return rule.test(getter, equalityTester, thisPos, offset)
} }
@ -156,7 +156,7 @@ data class RenderMatch(
* *
* [equalityTester] требуется для проверки раенства между "этим" тайлом и другим * [equalityTester] требуется для проверки раенства между "этим" тайлом и другим
*/ */
fun test(tileAccess: ITileChunk, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean { fun test(tileAccess: ITileAccess, equalityTester: EqualityRuleTester, thisPos: Vector2i): Boolean {
for (matcher in matchAllPoints) { for (matcher in matchAllPoints) {
if (!matcher.test(tileAccess, equalityTester, thisPos)) { if (!matcher.test(tileAccess, equalityTester, thisPos)) {
return false return false

View File

@ -14,8 +14,8 @@ fun AABB.encasingIntAABB(): AABBi {
fun AABB.encasingChunkPosAABB(): AABBi { fun AABB.encasingChunkPosAABB(): AABBi {
return AABBi( return AABBi(
Vector2i(ChunkPos.tileToChunkComponent(roundTowardsNegativeInfinity(mins.x)), ChunkPos.tileToChunkComponent(roundTowardsNegativeInfinity(mins.y))), Vector2i(ChunkPos.component(roundTowardsNegativeInfinity(mins.x)), ChunkPos.component(roundTowardsNegativeInfinity(mins.y))),
Vector2i(ChunkPos.tileToChunkComponent(roundTowardsPositiveInfinity(maxs.x)), ChunkPos.tileToChunkComponent(roundTowardsPositiveInfinity(maxs.y))), Vector2i(ChunkPos.component(roundTowardsPositiveInfinity(maxs.x)), ChunkPos.component(roundTowardsPositiveInfinity(maxs.y))),
) )
} }

View File

@ -1,8 +1,8 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.world.api.BackgroundView import ru.dbotthepony.kstarbound.world.api.BackgroundChunkView
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.ForegroundView import ru.dbotthepony.kstarbound.world.api.ForegroundChunkView
import ru.dbotthepony.kstarbound.world.api.IChunk import ru.dbotthepony.kstarbound.world.api.IChunk
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
@ -29,8 +29,8 @@ class CellView(
val bottom: IChunk?, val bottom: IChunk?,
val bottomRight: IChunk?, val bottomRight: IChunk?,
) : IChunk { ) : IChunk {
val backgroundView = BackgroundView(this) val backgroundView = BackgroundChunkView(this)
val foregroundView = ForegroundView(this) val foregroundView = ForegroundChunkView(this)
override fun getCell(x: Int, y: Int): IChunkCell { override fun getCell(x: Int, y: Int): IChunkCell {
val ix = x + CHUNK_SIZE val ix = x + CHUNK_SIZE

View File

@ -7,12 +7,12 @@ 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.BackgroundView import ru.dbotthepony.kstarbound.world.api.BackgroundChunkView
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_BITS
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZEd
import ru.dbotthepony.kstarbound.world.api.ForegroundView import ru.dbotthepony.kstarbound.world.api.ForegroundChunkView
import ru.dbotthepony.kstarbound.world.api.IChunk import ru.dbotthepony.kstarbound.world.api.IChunk
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,8 +38,7 @@ import kotlin.collections.HashSet
* *
* Весь игровой мир будет измеряться в Starbound Unit'ах * Весь игровой мир будет измеряться в Starbound Unit'ах
*/ */
abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(val world: WorldType, final override val pos: ChunkPos) : abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType, This>>(val world: WorldType, final override val pos: ChunkPos) : IChunk {
IChunk {
var changeset = 0 var changeset = 0
private set private set
var tileChangeset = 0 var tileChangeset = 0
@ -56,14 +55,34 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
var backgroundChangeset = 0 var backgroundChangeset = 0
private set private set
val left get() = pos.left protected open fun foregroundChanges() {
val right get() = pos.right changeset++
val top get() = pos.top cellChangeset++
val bottom get() = pos.bottom tileChangeset++
val topLeft get() = pos.topLeft
val topRight get() = pos.topRight collisionChangeset++
val bottomLeft get() = pos.bottomLeft foregroundChangeset++
val bottomRight get() = pos.bottomRight markPhysicsDirty()
}
protected open fun backgroundChanges() {
changeset++
cellChangeset++
tileChangeset++
backgroundChangeset++
}
protected inline fun forEachNeighbour(block: (This) -> Unit) {
world.chunkMap[pos.left]?.let(block)
world.chunkMap[pos.right]?.let(block)
world.chunkMap[pos.top]?.let(block)
world.chunkMap[pos.bottom]?.let(block)
world.chunkMap[pos.topLeft]?.let(block)
world.chunkMap[pos.topRight]?.let(block)
world.chunkMap[pos.bottomLeft]?.let(block)
world.chunkMap[pos.bottomRight]?.let(block)
}
val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble()) val aabb = aabbBase + Vector2d(pos.x * CHUNK_SIZE.toDouble(), pos.y * CHUNK_SIZE.toDouble())
@ -79,8 +98,8 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
protected val cells = Object2DArray(CHUNK_SIZE, CHUNK_SIZE, ::Cell) protected val cells = Object2DArray(CHUNK_SIZE, CHUNK_SIZE, ::Cell)
val backgroundView = BackgroundView(this) val backgroundView = BackgroundChunkView(this)
val foregroundView = ForegroundView(this) val foregroundView = ForegroundChunkView(this)
override fun getCell(x: Int, y: Int): IChunkCell { override fun getCell(x: Int, y: Int): IChunkCell {
return cells[x, y] return cells[x, y]
@ -92,16 +111,10 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
inner class Cell(val x: Int, val y: Int) : IChunkCell { inner class Cell(val x: Int, val y: Int) : IChunkCell {
inner class Tile(private val foreground: Boolean) : ITileState { inner class Tile(private val foreground: Boolean) : ITileState {
private fun change() { private fun change() {
changeset++
cellChangeset++
tileChangeset++
if (foreground) { if (foreground) {
collisionChangeset++ foregroundChanges()
foregroundChangeset++
markPhysicsDirty()
} else { } else {
backgroundChangeset++ backgroundChanges()
} }
} }

View File

@ -2,11 +2,9 @@ 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.api.CHUNK_SIZE_BITS
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE_FF
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
import kotlin.math.absoluteValue
private fun circulate(value: Int, bounds: Int): Int { private fun circulate(value: Int, bounds: Int): Int {
require(bounds > 0) { "Bounds must be positive ($bounds given)" } require(bounds > 0) { "Bounds must be positive ($bounds given)" }
@ -130,64 +128,48 @@ class ChunkPos(val x: Int, val y: Int) : Comparable<ChunkPos> {
return x.toLong() or (y.toLong() shl 32) return x.toLong() or (y.toLong() shl 32)
} }
fun fromTilePosition(input: IStruct2i): ChunkPos { fun longFromPosition(x: Int, y: Int): Long {
return toLong(component(x), component(y))
}
fun fromPosition(input: IStruct2i): ChunkPos {
val (x, y) = input val (x, y) = input
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y)) return ChunkPos(component(x), component(y))
} }
fun fromTilePosition(input: IStruct2i, xWrap: Int): ChunkPos { fun fromPosition(input: IStruct2d): ChunkPos {
val (x, y) = input val (x, y) = input
return ChunkPos(circulate(tileToChunkComponent(x), xWrap), tileToChunkComponent(y)) return fromPosition(x, y)
} }
fun fromTilePosition(input: IStruct2d): ChunkPos { fun fromPosition(x: Int, y: Int): ChunkPos {
val (x, y) = input return ChunkPos(component(x), component(y))
return fromTilePosition(x, y)
} }
fun fromTilePosition(input: IStruct2d, xWrap: Int): ChunkPos { fun fromPosition(x: Int, y: Int, xWrap: Int): ChunkPos {
val (x, y) = input return ChunkPos(circulate(component(x), xWrap), component(y))
return fromTilePosition(x, y, xWrap)
} }
fun fromTilePosition(x: Int, y: Int): ChunkPos { fun fromPosition(x: Double, y: Double): ChunkPos {
return ChunkPos(tileToChunkComponent(x), tileToChunkComponent(y))
}
fun fromTilePosition(x: Int, y: Int, xWrap: Int): ChunkPos {
return ChunkPos(circulate(tileToChunkComponent(x), xWrap), tileToChunkComponent(y))
}
fun fromTilePosition(x: Double, y: Double): ChunkPos {
return ChunkPos( return ChunkPos(
tileToChunkComponent(roundByAbsoluteValue(x)), component(roundByAbsoluteValue(x)),
tileToChunkComponent(roundByAbsoluteValue(y)) component(roundByAbsoluteValue(y))
) )
} }
fun fromTilePosition(x: Double, y: Double, xWrap: Int): ChunkPos { fun fromPosition(x: Double, y: Double, xWrap: Int): ChunkPos {
return ChunkPos( return ChunkPos(
circulate(tileToChunkComponent(roundByAbsoluteValue(x)), xWrap), circulate(component(roundByAbsoluteValue(x)), xWrap),
tileToChunkComponent(roundByAbsoluteValue(y)) component(roundByAbsoluteValue(y))
) )
} }
fun normalizeCoordinate(input: Int): Int { fun component(value: Int): Int {
val band = input and CHUNK_SIZE_FF if (value < 0) {
return -((-value) shr CHUNK_SIZE_BITS) - 1
if (band < 0) {
return band + CHUNK_SIZE_FF
} }
return band return value shr CHUNK_SIZE_BITS
}
fun tileToChunkComponent(comp: Int): Int {
if (comp < 0) {
return -(comp.absoluteValue shr CHUNK_SIZE_BITS) - 1
}
return comp shr CHUNK_SIZE_BITS
} }
} }
} }

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE
import ru.dbotthepony.kstarbound.world.api.IChunk import ru.dbotthepony.kstarbound.world.api.IChunk
import ru.dbotthepony.kstarbound.world.api.IChunkCell import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kstarbound.world.api.ITileChunk import ru.dbotthepony.kstarbound.world.api.ITileAccess
import ru.dbotthepony.kstarbound.world.api.ITileState import ru.dbotthepony.kstarbound.world.api.ITileState
import ru.dbotthepony.kvector.vector.Vector2i import ru.dbotthepony.kvector.vector.Vector2i
@ -35,7 +35,7 @@ fun IChunk.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE
} }
} }
fun ITileChunk.iterate(fromX: Int = 0, fromY: Int = 0, toX: Int = fromX + CHUNK_SIZE, toY: Int = fromY + CHUNK_SIZE): Iterator<Pair<Vector2i, ITileState>> { 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>> { return object : Iterator<Pair<Vector2i, ITileState>> {
private var x = fromX private var x = fromX
private var y = fromY private var y = fromY

View File

@ -0,0 +1,229 @@
package ru.dbotthepony.kstarbound.world
import com.google.common.collect.ImmutableList
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.api.ICellAccess
import ru.dbotthepony.kstarbound.world.api.IChunkCell
import ru.dbotthepony.kvector.arrays.Double2DArray
import ru.dbotthepony.kvector.vector.Vector2d
import ru.dbotthepony.kvector.vector.Vector2i
import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt
import kotlin.math.sin
const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
data class RayCastResult(
val traversedTiles: List<Pair<Vector2i, IChunkCell>>,
val hitTile: Pair<Vector2i, IChunkCell>?,
val fraction: Double
)
private fun makeDirFan(step: Double): List<Vector2d> {
var i = 0.0
val result = ImmutableList.builder<Vector2d>()
while (i < 360.0) {
i += step
result.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
}
return result.build()
}
private val potatoDirFan by lazy { makeDirFan(4.0) }
private val veryRoughDirFan by lazy { makeDirFan(3.0) }
private val roughDirFan by lazy { makeDirFan(2.0) }
private val dirFan by lazy { makeDirFan(1.0) }
private val preciseFan by lazy { makeDirFan(0.5) }
private val veryPreciseFan by lazy { makeDirFan(0.25) }
private fun chooseLightRayFan(size: Double): List<Vector2d> {
return when (size) {
in 0.0 .. 8.0 -> potatoDirFan
in 8.0 .. 16.0 -> veryRoughDirFan
in 16.0 .. 24.0 -> roughDirFan
in 24.0 .. 48.0 -> dirFan
in 48.0 .. 96.0 -> preciseFan
// in 32.0 .. 48.0 -> veryPreciseFan
else -> veryPreciseFan
}
}
/**
* [HIT] - луч попал по объекту и трассировка прекращается; объект записывается в коллекцию объектов, в которые попал луч.
*
* [HIT_SKIP] - луч попал по объекту и трассировка прекращается; объект не записывается в коллекцию объектов, в которые попал луч.
*
* [SKIP] - луч не попал по объекту, объект не записывается в коллекцию объектов, в которые попал луч.
*
* [CONTINUE] - луч не попал по объекту; объект записывается в коллекцию объектов, в которые попал луч.
*/
enum class RayFilterResult {
HIT,
HIT_SKIP,
SKIP,
CONTINUE;
companion object {
fun of(boolean: Boolean): RayFilterResult {
return if (boolean) HIT else CONTINUE
}
}
}
fun interface TileRayFilter {
fun test(state: IChunkCell, fraction: Double, position: Vector2i): RayFilterResult
}
/**
* Считает все тайлы неблокирующими
*/
val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE }
/**
* Попадает по первому не-пустому тайлу
*/
val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material != null) }
/**
* Попадает по первому пустому тайлу
*/
val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material == null) }
/**
* Попадает по первому тайлу который блокирует проход света
*/
val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material?.renderParameters?.lightTransparent == false) }
/**
* Бросает луч напротив тайлов мира с заданными позициями и фильтром
*/
fun ICellAccess.castRayNaive(
rayStart: Vector2d,
rayEnd: Vector2d,
filter: TileRayFilter = AnythingRayFilter
): RayCastResult {
if (rayStart == rayEnd) {
return RayCastResult(listOf(), null, 1.0)
}
var t = 0.0
val dir = rayEnd - rayStart
val inc = 0.5 / dir.length
val tiles = LinkedList<Pair<Vector2i, IChunkCell>>()
var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE)
var hitTile: Pair<Vector2i, IChunkCell>? = null
while (t < 1.0) {
val (x, y) = rayStart + dir * t
val tilePos = Vector2i(x.roundToInt(), y.roundToInt())
if (tilePos != prev) {
val tile = getCell(tilePos)
when (filter.test(tile, t, tilePos)) {
RayFilterResult.HIT -> {
hitTile = tilePos to tile
tiles.add(hitTile)
break
}
RayFilterResult.HIT_SKIP -> {
hitTile = tilePos to tile
break
}
RayFilterResult.SKIP -> {}
RayFilterResult.CONTINUE -> tiles.add(tilePos to tile)
}
prev = tilePos
}
t += inc
}
return RayCastResult(tiles, hitTile, t)
}
/**
* Бросает луч напротив тайлов мира с заданной позицией, направлением и фильтром
*/
fun ICellAccess.castRayNaive(
rayPosition: Vector2d,
direction: Vector2d,
length: Double,
filter: TileRayFilter = AnythingRayFilter
): RayCastResult {
return castRayNaive(rayPosition, rayPosition + direction.unitVector * length, filter)
}
/**
* Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением.
*
* Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы.
*/
fun ICellAccess.rayLightNaive(
position: Vector2d,
direction: Vector2d,
intensity: Double,
falloffByTile: Double = 2.0,
falloffByTravel: Double = 1.0,
): List<Pair<Double, Vector2i>> {
val result = ArrayList<Pair<Double, Vector2i>>()
var currentIntensity = intensity
castRayNaive(position, direction, intensity) { state, t, pos ->
if (state.foreground.material?.renderParameters?.lightTransparent == false) {
currentIntensity -= falloffByTile
} else {
currentIntensity -= falloffByTravel
}
//result.add(currentIntensity to pos)
if (currentIntensity <= 0.0) {
return@castRayNaive RayFilterResult.HIT_SKIP
} else {
return@castRayNaive RayFilterResult.SKIP
}
}
return result
}
/**
* Трассирует лучи света вокруг себя с заданной позицией, интенсивностью,
* падением интенсивности за проход сквозь тайл [falloffByTile] и
* падением интенсивности за проход по пустому месту [falloffByTravel].
*/
fun ICellAccess.rayLightCircleNaive(
position: Vector2d,
intensity: Double,
falloffByTile: Double = 2.0,
falloffByTravel: Double = 1.0,
): Double2DArray {
val combinedResult = Double2DArray.allocate(intensity.roundToInt() * 2, intensity.roundToInt() * 2)
val baselineX = position.x.roundToInt() - intensity.roundToInt()
val baselineY = position.y.roundToInt() - intensity.roundToInt()
val dirs = chooseLightRayFan(intensity)
val mul = 1.0 / dirs.size
for (dir in dirs) {
val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel)
for (pair in result2) {
combinedResult[pair.second.y - baselineY, pair.second.x - baselineX] += pair.first * mul
}
}
return combinedResult
}

View File

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

View File

@ -1,8 +1,7 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.kbox2d.api.ContactImpulse import ru.dbotthepony.kbox2d.api.ContactImpulse
import ru.dbotthepony.kbox2d.api.IContactFilter import ru.dbotthepony.kbox2d.api.IContactFilter
import ru.dbotthepony.kbox2d.api.IContactListener import ru.dbotthepony.kbox2d.api.IContactListener
@ -10,128 +9,212 @@ import ru.dbotthepony.kbox2d.api.Manifold
import ru.dbotthepony.kbox2d.dynamics.B2Fixture import ru.dbotthepony.kbox2d.dynamics.B2Fixture
import ru.dbotthepony.kbox2d.dynamics.B2World import ru.dbotthepony.kbox2d.dynamics.B2World
import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact
import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT
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.BackgroundAccessView
import ru.dbotthepony.kstarbound.world.api.CHUNK_SIZE 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.ForegroundAccessView
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.entities.Entity import ru.dbotthepony.kstarbound.world.entities.Entity
import ru.dbotthepony.kvector.arrays.Double2DArray import ru.dbotthepony.kvector.api.IStruct2d
import ru.dbotthepony.kvector.api.IStruct2i
import ru.dbotthepony.kvector.arrays.Int2DArray import ru.dbotthepony.kvector.arrays.Int2DArray
import ru.dbotthepony.kvector.arrays.Object2DArray
import ru.dbotthepony.kvector.util2d.AABB 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.util.LinkedList
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.roundToInt
import kotlin.math.sin
const val EARTH_FREEFALL_ACCELERATION = 9.8312 / METRES_IN_STARBOUND_UNIT
data class RayCastResult(
val traversedTiles: List<Pair<Vector2i, IChunkCell>>,
val hitTile: Pair<Vector2i, IChunkCell>?,
val fraction: Double
)
private fun makeDirFan(step: Double): List<Vector2d> {
var i = 0.0
val result = ImmutableList.builder<Vector2d>()
while (i < 360.0) {
i += step
result.add(Vector2d(cos(i / 180.0 * PI), sin(i / 180.0 * PI)))
}
return result.build()
}
private val potatoDirFan by lazy { makeDirFan(4.0) }
private val veryRoughDirFan by lazy { makeDirFan(3.0) }
private val roughDirFan by lazy { makeDirFan(2.0) }
private val dirFan by lazy { makeDirFan(1.0) }
private val preciseFan by lazy { makeDirFan(0.5) }
private val veryPreciseFan by lazy { makeDirFan(0.25) }
private fun chooseLightRayFan(size: Double): List<Vector2d> {
return when (size) {
in 0.0 .. 8.0 -> potatoDirFan
in 8.0 .. 16.0 -> veryRoughDirFan
in 16.0 .. 24.0 -> roughDirFan
in 24.0 .. 48.0 -> dirFan
in 48.0 .. 96.0 -> preciseFan
// in 32.0 .. 48.0 -> veryPreciseFan
else -> veryPreciseFan
}
}
/**
* [HIT] - луч попал по объекту и трассировка прекращается; объект записывается в коллекцию объектов, в которые попал луч.
*
* [HIT_SKIP] - луч попал по объекту и трассировка прекращается; объект не записывается в коллекцию объектов, в которые попал луч.
*
* [SKIP] - луч не попал по объекту, объект не записывается в коллекцию объектов, в которые попал луч.
*
* [CONTINUE] - луч не попал по объекту; объект записывается в коллекцию объектов, в которые попал луч.
*/
enum class RayFilterResult {
HIT,
HIT_SKIP,
SKIP,
CONTINUE;
companion object {
fun of(boolean: Boolean): RayFilterResult {
return if (boolean) HIT else CONTINUE
}
}
}
fun interface TileRayFilter {
fun test(state: IChunkCell, fraction: Double, position: Vector2i): RayFilterResult
}
/**
* Считает все тайлы неблокирующими
*/
val AnythingRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.CONTINUE }
/**
* Попадает по первому не-пустому тайлу
*/
val NonSolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material != null) }
/**
* Попадает по первому пустому тайлу
*/
val SolidRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material == null) }
/**
* Попадает по первому тайлу который блокирует проход света
*/
val LineOfSightRayFilter = TileRayFilter { state, t, pos -> RayFilterResult.of(state.foreground.material?.renderParameters?.lightTransparent == false) }
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,
val widthInChunks: Int val size: Vector2i?,
val loopX: Boolean,
val loopY: Boolean
) { ) {
protected val chunkMap = Long2ObjectOpenHashMap<ChunkType>() abstract class AbstractCoordinatesWrapper {
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 chunk(value: Double): Double
/** protected fun positiveModulo(a: Int, b: Int): Int {
* Является ли мир "сферическим" val result = a % b
* return if (result < 0) result + b else result
* Данный флаг говорит о том, что [widthInChunks] имеет осмысленное значение, }
* попытка получить чанк с X координатой меньше нуля или больше [widthInChunks]
* приведёт к замыканию на конец/начало мира соответственно protected fun positiveModulo(a: Double, b: Int): Double {
*/ val result = a % b
val isCircular = widthInChunks > 0 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 chunk(value: Double): Double = value
}
class CoordinatesWrapper(private val cell: Int, private val chunk: Int) : AbstractCoordinatesWrapper() {
override fun cell(value: Int): Int {
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)
}
override fun chunk(value: Double): Double {
return positiveModulo(value, chunk)
}
}
class CoordinatesClamper(private val cell: Int, private val chunk: Int) : AbstractCoordinatesWrapper() {
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)
}
override fun chunk(value: Double): Double {
return value.coerceIn(0.0, chunk - 1.0)
}
}
abstract inner class ChunkMap : ICellAccess {
abstract val x: AbstractCoordinatesWrapper
abstract val y: AbstractCoordinatesWrapper
val backgroundView = BackgroundAccessView(this)
val foregroundView = ForegroundAccessView(this)
abstract operator fun get(x: Int, y: Int): ChunkType?
operator fun get(pos: ChunkPos) = get(pos.x, pos.y)
override fun getCell(x: Int, y: Int): IChunkCell {
return get(ChunkPos.component(x), ChunkPos.component(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)
fun cellToGrid(x: Int, y: Int) = ChunkPos.fromPosition(this.x.cell(x), this.y.cell(y))
fun cellToGrid(x: Double, y: Double) = ChunkPos.fromPosition(this.x.cell(x), this.y.cell(y))
fun cellToGrid(position: IStruct2i) = cellToGrid(position.component1(), position.component2())
fun cellToGrid(position: IStruct2d) = cellToGrid(position.component1(), position.component2())
fun computeIfAbsent(pos: ChunkPos) = computeIfAbsent(pos.x, pos.y)
fun computeIfAbsent(x: Int, y: Int): ChunkType {
val existing = get(x, y)
if (existing != null)
return existing
val pos = ChunkPos(this.x.chunk(x), this.y.chunk(y))
val chunk = chunkFactory(pos)
val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) {
val (ex, ey) = ent.position
if (ChunkPos.fromPosition(this.x.cell(ex), this.y.cell(ey)) == pos) {
orphanedInThisChunk.add(ent)
}
}
for (ent in orphanedInThisChunk) {
ent.chunk = chunk
}
set(x, y, chunk)
return chunk
}
}
inner class InfiniteChunkMap : ChunkMap() {
private val map = Long2ObjectOpenHashMap<ChunkType>()
override fun get(x: Int, y: Int): ChunkType? {
return map[ChunkPos.toLong(x, y)]
}
override fun set(x: Int, y: Int, chunk: ChunkType) {
map[ChunkPos.toLong(x, y)] = chunk
}
override fun remove(x: Int, y: Int) {
map.remove(ChunkPos.toLong(x, y))
}
override val x = PassthroughWrapper
override val y = PassthroughWrapper
}
inner class RectChunkMap : ChunkMap() {
val width = size!!.x
val height = size!!.y
val widthInChunks = if (width and CHUNK_SIZE_MASK == 0) width / CHUNK_SIZE else width / CHUNK_SIZE + 1
val heightInChunks = if (height and CHUNK_SIZE_MASK == 0) height / CHUNK_SIZE else height / CHUNK_SIZE + 1
override val x: AbstractCoordinatesWrapper = if (loopX) CoordinatesWrapper(width, widthInChunks) else CoordinatesClamper(width, widthInChunks)
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? {
return map[this.x.chunk(x), this.y.chunk(y)]
}
override fun getCell(x: Int, y: Int): IChunkCell {
if (x < 0 || y < 0) return IChunkCell.Companion
return get(x ushr CHUNK_SIZE_BITS, y ushr CHUNK_SIZE_BITS)?.getCell(x and CHUNK_SIZE_MASK, y and CHUNK_SIZE_MASK) ?: IChunkCell.Companion
}
override fun set(x: Int, y: Int, chunk: ChunkType) {
map[this.x.chunk(x), this.y.chunk(y)] = chunk
}
override fun remove(x: Int, y: Int) {
map[this.x.chunk(x), this.y.chunk(y)] = null
}
}
val chunkMap: ChunkMap = if (size != null) RectChunkMap() else InfiniteChunkMap()
/** /**
* Chunks, which have their collision mesh changed * Chunks, which have their collision mesh changed
*/ */
val dirtyPhysicsChunks = HashSet<ChunkType>() val dirtyPhysicsChunks = ObjectOpenHashSet<ChunkType>()
val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION)) val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION))
@ -293,270 +376,20 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
*/ */
var gravity by physics::gravity var gravity by physics::gravity
protected abstract fun chunkFactory( protected abstract fun chunkFactory(pos: ChunkPos): ChunkType
pos: ChunkPos,
): ChunkType
/**
* Возвращает чанк на указанной позиции если он существует
*/
open operator fun get(pos: ChunkPos): ChunkType? {
if (!isCircular) {
return chunkMap[pos.toLong()]
}
@Suppress("Name_Shadowing")
val pos = pos.circular(widthInChunks)
return chunkMap[pos.toLong()]
}
open fun getInstantTuple(pos: ChunkPos): IWorldChunkTuple<This, ChunkType>? {
return this[pos]?.let { InstantWorldChunkTuple(this as This, it) }
}
/**
* Возвращает чанк на заданной позиции, создаёт его если он не существует
*/
open fun computeIfAbsent(pos: ChunkPos): ChunkType {
@Suppress("Name_Shadowing")
val pos = if (isCircular) pos.circular(widthInChunks) else pos
return chunkMap.computeIfAbsent(pos.toLong(), Long2ObjectFunction {
val chunk = chunkFactory(pos)
val orphanedInThisChunk = ArrayList<Entity>()
for (ent in orphanedEntities) {
val cPos = if (isCircular) ChunkPos.fromTilePosition(ent.position, widthInChunks) else ChunkPos.fromTilePosition(ent.position)
if (cPos == pos) {
orphanedInThisChunk.add(ent)
}
}
for (ent in orphanedInThisChunk) {
ent.chunk = chunk
}
return@Long2ObjectFunction chunk
})
}
/**
* Позволяет получать чанки/тайлы с минимальным кешем. Если один чанк считывается очень большое число раз,
* то использование этого класса сильно ускорит работу.
*
* Так же реализует raycasting методы.
*/
inner class CachedGetter {
private var lastChunk: ChunkType? = null
private var lastPos: ChunkPos? = null
operator fun get(pos: ChunkPos): ChunkType? {
if (lastPos == pos) {
return lastChunk
}
lastChunk = this@World[pos]
lastPos = pos
return lastChunk
}
fun getCell(pos: Vector2i): IChunkCell? {
return get(ChunkPos.fromTilePosition(pos))?.getCell(ChunkPos.normalizeCoordinate(pos.x), ChunkPos.normalizeCoordinate(pos.y))
}
/**
* Бросает луч напротив тайлов мира с заданными позициями и фильтром
*/
fun castRayNaive(
rayStart: Vector2d,
rayEnd: Vector2d,
filter: TileRayFilter = AnythingRayFilter
): RayCastResult {
if (rayStart == rayEnd) {
return RayCastResult(listOf(), null, 1.0)
}
var t = 0.0
val dir = rayEnd - rayStart
val inc = 0.5 / dir.length
val tiles = LinkedList<Pair<Vector2i, IChunkCell>>()
var prev = Vector2i(Int.MIN_VALUE, Int.MAX_VALUE)
var hitTile: Pair<Vector2i, IChunkCell>? = null
while (t < 1.0) {
val (x, y) = rayStart + dir * t
val tilePos = Vector2i(x.roundToInt(), y.roundToInt())
if (tilePos != prev) {
val tile = getCell(tilePos) ?: IChunkCell.Companion
when (filter.test(tile, t, tilePos)) {
RayFilterResult.HIT -> {
hitTile = tilePos to tile
tiles.add(hitTile)
break
}
RayFilterResult.HIT_SKIP -> {
hitTile = tilePos to tile
break
}
RayFilterResult.SKIP -> {}
RayFilterResult.CONTINUE -> tiles.add(tilePos to tile)
}
prev = tilePos
}
t += inc
}
return RayCastResult(tiles, hitTile, t)
}
/**
* Бросает луч напротив тайлов мира с заданной позицией, направлением и фильтром
*/
fun castRayNaive(
rayPosition: Vector2d,
direction: Vector2d,
length: Double,
filter: TileRayFilter = AnythingRayFilter
): RayCastResult {
return castRayNaive(rayPosition, rayPosition + direction.unitVector * length, filter)
}
/**
* Выпускает луч света с заданной силой (определяет длину луча и способность проходить сквозь тайлы), позицией и направлением.
*
* Позволяет указать отдельно [falloffByTile] потерю силы света при прохождении через тайлы.
*/
fun rayLightNaive(
position: Vector2d,
direction: Vector2d,
intensity: Double,
falloffByTile: Double = 2.0,
falloffByTravel: Double = 1.0,
): List<Pair<Double, Vector2i>> {
val result = ArrayList<Pair<Double, Vector2i>>()
var currentIntensity = intensity
castRayNaive(position, direction, intensity) { state, t, pos ->
if (state.foreground.material?.renderParameters?.lightTransparent == false) {
currentIntensity -= falloffByTile
} else {
currentIntensity -= falloffByTravel
}
//result.add(currentIntensity to pos)
if (currentIntensity <= 0.0) {
return@castRayNaive RayFilterResult.HIT_SKIP
} else {
return@castRayNaive RayFilterResult.SKIP
}
}
return result
}
/**
* Трассирует лучи света вокруг себя с заданной позицией, интенсивностью,
* падением интенсивности за проход сквозь тайл [falloffByTile] и
* падением интенсивности за проход по пустому месту [falloffByTravel].
*/
fun rayLightCircleNaive(
position: Vector2d,
intensity: Double,
falloffByTile: Double = 2.0,
falloffByTravel: Double = 1.0,
): Double2DArray {
val combinedResult = Double2DArray.allocate(intensity.roundToInt() * 2, intensity.roundToInt() * 2)
val baselineX = position.x.roundToInt() - intensity.roundToInt()
val baselineY = position.y.roundToInt() - intensity.roundToInt()
val dirs = chooseLightRayFan(intensity)
val mul = 1.0 / dirs.size
for (dir in dirs) {
val result2 = rayLightNaive(position, dir, intensity, falloffByTile, falloffByTravel)
for (pair in result2) {
combinedResult[pair.second.y - baselineY, pair.second.x - baselineX] += pair.first * mul
}
}
return combinedResult
}
}
/**
* @see CachedGetter.castRayNaive
*/
fun castRayNaive(
rayStart: Vector2d,
rayEnd: Vector2d,
filter: TileRayFilter = AnythingRayFilter
): RayCastResult {
return CachedGetter().castRayNaive(rayStart, rayEnd, filter)
}
/**
* @see CachedGetter.castRayNaive
*/
fun castRayNaive(
rayPosition: Vector2d,
direction: Vector2d,
length: Double,
filter: TileRayFilter = AnythingRayFilter
): RayCastResult {
return CachedGetter().castRayNaive(rayPosition, direction, length, filter)
}
/**
* @see CachedGetter.rayLightNaive
*/
fun rayLightNaive(
position: Vector2d,
direction: Vector2d,
intensity: Double,
falloffByTile: Double = 2.0,
falloffByTravel: Double = 1.0,
): List<Pair<Double, Vector2i>> {
return CachedGetter().rayLightNaive(position, direction, intensity, falloffByTile, falloffByTravel)
}
/**
* @see CachedGetter.rayLightCircleNaive
*/
fun rayLightCircleNaive(
position: Vector2d,
intensity: Double,
falloffByTile: Double = 2.0,
falloffByTravel: Double = 2.0,
): Double2DArray {
return CachedGetter().rayLightCircleNaive(position, intensity, falloffByTile, falloffByTravel)
}
fun getView(pos: ChunkPos): CellView { fun getView(pos: ChunkPos): CellView {
val tuple = get(pos)?.let { InstantWorldChunkTuple(this as This, it) }
return CellView( return CellView(
pos = pos, pos = pos,
center = tuple?.chunk, center = chunkMap[pos],
left = tuple?.left?.chunk, left = chunkMap[pos.left],
top = tuple?.top?.chunk, top = chunkMap[pos.top],
topLeft = tuple?.topLeft?.chunk, topLeft = chunkMap[pos.topLeft],
topRight = tuple?.topRight?.chunk, topRight = chunkMap[pos.topRight],
right = tuple?.right?.chunk, right = chunkMap[pos.right],
bottom = tuple?.bottom?.chunk, bottom = chunkMap[pos.bottom],
bottomLeft = tuple?.bottomLeft?.chunk, bottomLeft = chunkMap[pos.bottomLeft],
bottomRight = tuple?.bottomRight?.chunk, bottomRight = chunkMap[pos.bottomRight],
) )
} }
@ -567,9 +400,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val output = ArrayList<ChunkType>() val output = ArrayList<ChunkType>()
for (pos in boundingBox.chunkPositions) { for (pos in boundingBox.chunkPositions) {
val chunk = get(pos) val chunk = chunkMap[pos]
if (chunk != null) { if (chunk != null && chunk !in output) {
output.add(chunk) output.add(chunk)
} }
} }
@ -584,9 +417,9 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val output = ArrayList<Pair<ChunkPos, ChunkType>>() val output = ArrayList<Pair<ChunkPos, ChunkType>>()
for (pos in boundingBox.chunkPositions) { for (pos in boundingBox.chunkPositions) {
val chunk = get(pos) val chunk = chunkMap[pos]
if (chunk != null) { if (chunk != null && !output.any { it.second === chunk }) {
output.add(pos to chunk) output.add(pos to chunk)
} }
} }
@ -715,7 +548,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val lightmap = Int2DArray.allocate(lightIntensity * 2 + 1, lightIntensity * 2 + 1) val lightmap = Int2DArray.allocate(lightIntensity * 2 + 1, lightIntensity * 2 + 1)
val view = getView(ChunkPos.fromTilePosition(lightPosition)) val view = getView(ChunkPos.fromPosition(lightPosition))
floodLightInto( floodLightInto(
lightmap, lightmap,

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
const val CHUNK_SIZE_BITS = 5 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 = 1 shl CHUNK_SIZE_BITS // 32
const val CHUNK_SIZE_FF = CHUNK_SIZE - 1 const val CHUNK_SIZE_FF = CHUNK_SIZE - 1
const val CHUNK_SIZEf = CHUNK_SIZE.toFloat() const val CHUNK_SIZEf = CHUNK_SIZE.toFloat()

View File

@ -0,0 +1,9 @@
package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kvector.api.IStruct2i
interface ICellAccess {
// relative
fun getCell(x: Int, y: Int): IChunkCell
fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2())
}

View File

@ -4,13 +4,9 @@ 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 IChunk { interface IChunk : ICellAccess {
val pos: ChunkPos val pos: ChunkPos
// relative
fun getCell(x: Int, y: Int): IChunkCell
fun getCell(pos: IStruct2i) = getCell(pos.component1(), pos.component2())
/** /**
* Возвращает псевдослучайное Long для заданной позиции * Возвращает псевдослучайное Long для заданной позиции
* *
@ -47,4 +43,4 @@ interface IChunk {
* Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции
*/ */
fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y)
} }

View File

@ -0,0 +1,36 @@
package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kvector.api.IStruct2i
// for getting tiles directly, avoiding manual layer specification
interface ITileAccess : ICellAccess {
// relative
fun getTile(x: Int, y: Int): ITileState
fun getTile(pos: IStruct2i) = getTile(pos.component1(), pos.component2())
}
interface ITileChunk : IChunk, ITileAccess
class ForegroundChunkView(private val parent: IChunk) : ITileChunk, IChunk by parent {
override fun getTile(x: Int, y: Int): ITileState {
return parent.getCell(x, y).foreground
}
}
class BackgroundChunkView(private val parent: IChunk) : ITileChunk, IChunk by parent {
override fun getTile(x: Int, y: Int): ITileState {
return parent.getCell(x, y).background
}
}
class ForegroundAccessView(private val parent: ICellAccess) : ITileAccess, ICellAccess by parent {
override fun getTile(x: Int, y: Int): ITileState {
return parent.getCell(x, y).foreground
}
}
class BackgroundAccessView(private val parent: ICellAccess) : ITileAccess, ICellAccess by parent {
override fun getTile(x: Int, y: Int): ITileState {
return parent.getCell(x, y).background
}
}

View File

@ -1,22 +0,0 @@
package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kvector.api.IStruct2i
// 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
}
}

View File

@ -2,7 +2,6 @@ package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kstarbound.defs.DamageType import ru.dbotthepony.kstarbound.defs.DamageType
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2d
@ -95,7 +94,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
return return
} }
val chunkPos = if (world.isCircular) ChunkPos.fromTilePosition(position, world.widthInChunks) else ChunkPos.fromTilePosition(position) val chunkPos = world.chunkMap.cellToGrid(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})")
@ -121,16 +120,16 @@ abstract class Entity(override val world: World<*, *>) : IEntity {
return return
val old = field val old = field
field = value field = Vector2d(world.chunkMap.x.cell(value.x), world.chunkMap.x.cell(value.y))
movement.notifyPositionChanged() movement.notifyPositionChanged()
if (isSpawned && !isRemoved) { if (isSpawned && !isRemoved) {
val oldChunkPos = ChunkPos.fromTilePosition(old) val oldChunkPos = world.chunkMap.cellToGrid(old)
val newChunkPos = ChunkPos.fromTilePosition(value) val newChunkPos = world.chunkMap.cellToGrid(field)
if (oldChunkPos != newChunkPos) { if (oldChunkPos != newChunkPos) {
chunk = world[newChunkPos] chunk = world.chunkMap[newChunkPos]
} }
} }
} }
@ -155,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[ChunkPos.fromTilePosition(position)] chunk = world.chunkMap[world.chunkMap.cellToGrid(position)]
if (chunk == null) { if (chunk == null) {
world.orphanedEntities.add(this) world.orphanedEntities.add(this)

View File

@ -51,21 +51,21 @@ object MathTests {
@Test @Test
@DisplayName("ChunkPos class tests") @DisplayName("ChunkPos class tests")
fun chunkPosTests() { fun chunkPosTests() {
check(ChunkPos.fromTilePosition(0, 0) == ChunkPos(0, 0)) check(ChunkPos.fromPosition(0, 0) == ChunkPos(0, 0))
check(ChunkPos.fromTilePosition(1, 0) == ChunkPos(0, 0)) check(ChunkPos.fromPosition(1, 0) == ChunkPos(0, 0))
check(ChunkPos.fromTilePosition(0, 1) == ChunkPos(0, 0)) check(ChunkPos.fromPosition(0, 1) == ChunkPos(0, 0))
check(ChunkPos.fromTilePosition(1, 1) == ChunkPos(0, 0)) check(ChunkPos.fromPosition(1, 1) == ChunkPos(0, 0))
check(ChunkPos.fromTilePosition(-1, 1) == ChunkPos(-1, 0)) check(ChunkPos.fromPosition(-1, 1) == ChunkPos(-1, 0))
check(ChunkPos.fromTilePosition(-1, -1) == ChunkPos(-1, -1)) check(ChunkPos.fromPosition(-1, -1) == ChunkPos(-1, -1))
check(ChunkPos.fromTilePosition(CHUNK_SIZE_FF, 0) == ChunkPos(0, 0)) check(ChunkPos.fromPosition(CHUNK_SIZE_FF, 0) == ChunkPos(0, 0))
check(ChunkPos.fromTilePosition(CHUNK_SIZE, 0) == ChunkPos(1, 0)) check(ChunkPos.fromPosition(CHUNK_SIZE, 0) == ChunkPos(1, 0))
check(ChunkPos.fromTilePosition(0, CHUNK_SIZE_FF) == ChunkPos(0, 0)) check(ChunkPos.fromPosition(0, CHUNK_SIZE_FF) == ChunkPos(0, 0))
check(ChunkPos.fromTilePosition(0, CHUNK_SIZE) == ChunkPos(0, 1)) check(ChunkPos.fromPosition(0, CHUNK_SIZE) == ChunkPos(0, 1))
check(ChunkPos.fromTilePosition(-CHUNK_SIZE_FF, 0) == ChunkPos(-1, 0)) { ChunkPos.fromTilePosition(-CHUNK_SIZE_FF, 0).toString() } check(ChunkPos.fromPosition(-CHUNK_SIZE_FF, 0) == ChunkPos(-1, 0)) { ChunkPos.fromPosition(-CHUNK_SIZE_FF, 0).toString() }
check(ChunkPos.fromTilePosition(-CHUNK_SIZE, 0) == ChunkPos(-2, 0)) { ChunkPos.fromTilePosition(-CHUNK_SIZE, 0) } check(ChunkPos.fromPosition(-CHUNK_SIZE, 0) == ChunkPos(-2, 0)) { ChunkPos.fromPosition(-CHUNK_SIZE, 0) }
check(ChunkPos.fromTilePosition(0, -CHUNK_SIZE_FF) == ChunkPos(0, -1)) { ChunkPos.fromTilePosition(0, -CHUNK_SIZE_FF) } check(ChunkPos.fromPosition(0, -CHUNK_SIZE_FF) == ChunkPos(0, -1)) { ChunkPos.fromPosition(0, -CHUNK_SIZE_FF) }
check(ChunkPos.fromTilePosition(0, -CHUNK_SIZE) == ChunkPos(0, -2)) { ChunkPos.fromTilePosition(0, -CHUNK_SIZE) } check(ChunkPos.fromPosition(0, -CHUNK_SIZE) == ChunkPos(0, -2)) { ChunkPos.fromPosition(0, -CHUNK_SIZE) }
} }
} }