Proper chunk tracking

This commit is contained in:
DBotThePony 2024-03-27 23:09:52 +07:00
parent 82b09721fc
commit a5192bc551
Signed by: DBot
GPG Key ID: DCC23B5715498507
15 changed files with 240 additions and 158 deletions

View File

@ -54,7 +54,6 @@ import ru.dbotthepony.kstarbound.client.gl.shader.UberShader
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kstarbound.client.gl.vertex.VertexBuilder
import ru.dbotthepony.kstarbound.client.input.UserInput
import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket
import ru.dbotthepony.kstarbound.client.render.Camera
import ru.dbotthepony.kstarbound.client.render.Font
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
@ -947,9 +946,6 @@ class StarboundClient private constructor(val clientID: Int) : Closeable {
val activeConnection = activeConnection
if (activeConnection != null && !activeConnection.isLegacy && activeConnection.channel.isOpen)
activeConnection.send(TrackedPositionPacket(camera.pos))
uberShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }
fontShaderPrograms.forValidRefs { it.viewMatrix = viewportMatrixScreen }

View File

@ -243,7 +243,7 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument
return
}
context.returnBuffer.setTo(GlobalDefaults.client.defaultFootstepSound.random())
context.returnBuffer.setTo(GlobalDefaults.client.defaultFootstepSound.map({ it }, { it.random() }))
}
private fun materialHealth(context: ExecutionContext, arguments: ArgumentIterator) {

View File

@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement
@ -162,7 +163,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
val clientStateGroup = MasterElement(NetworkedGroup(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientSpectatingEntities))
// in tiles
fun trackingRegions(): List<AABBi> {
fun trackingTileRegions(): List<AABBi> {
val result = ArrayList<AABBi>()
val mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder)
@ -178,7 +179,15 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
if (playerEntity != null) {
// add an extra region the size of the window centered on the player's position to prevent nearby sectors
// being unloaded due to camera panning or centering on other entities
result.add(window + Vector2i(playerEntity.position.x.roundToInt(), playerEntity.position.y.roundToInt()))
val diff = Vector2d(window.width / 2.0, window.height / 2.0)
val pmins = playerEntity.position - diff
val pmaxs = playerEntity.position + diff
result.add(AABBi(
Vector2i(pmins.x.roundToInt(), pmins.y.roundToInt()),
Vector2i(pmaxs.x.roundToInt(), pmaxs.y.roundToInt()),
))
}
for (entity in clientSpectatingEntities.get()) {

View File

@ -1,7 +1,6 @@
package ru.dbotthepony.kstarbound.network
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.channel.ChannelDuplexHandler
import io.netty.channel.ChannelHandlerContext
@ -48,8 +47,6 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectReq
import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldClientStateUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.WorldStartAcknowledgePacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket
import java.io.BufferedInputStream
import java.io.DataInputStream
import java.io.DataOutputStream
@ -358,8 +355,6 @@ class PacketRegistry(val isLegacy: Boolean) {
NATIVE.add(::JoinWorldPacket)
NATIVE.add(::ChunkCellsPacket)
NATIVE.add(::ForgetChunkPacket)
NATIVE.add(::TrackedPositionPacket)
NATIVE.add(::TrackedSizePacket)
NATIVE.add(::SpawnWorldObjectPacket)
NATIVE.add(::ForgetEntityPacket)
NATIVE.add(::UniverseTimeUpdatePacket)

View File

@ -4,21 +4,16 @@ import com.google.gson.JsonObject
import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.defs.WarpAlias
import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
@ -28,6 +23,7 @@ import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.PlayerWarpResultPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.server.world.WorldStorage
@ -37,7 +33,6 @@ import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import java.io.DataOutputStream
import java.util.HashMap
@ -76,33 +71,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
return "ServerConnection[ID=$connectionID channel=$channel / $ship]"
}
var trackedPosition: Vector2d = Vector2d.ZERO
set(value) {
if (field != value) {
field = value
needsToRecomputeTrackedChunks = true
}
}
var trackedPositionChunk: ChunkPos = ChunkPos.ZERO
private set
var trackedChunksWidth = 4
set(value) {
if (field != value) {
field = value
needsToRecomputeTrackedChunks = true
}
}
var trackedChunksHeight = 4
set(value) {
if (field != value) {
field = value
needsToRecomputeTrackedChunks = true
}
}
private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<WorldStorage>()
@ -123,23 +91,17 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipChunks.putAll(chunks)
}
private val tickets = HashMap<ChunkPos, ServerWorld.ITicket>()
private val tickets = HashMap<ChunkPos, Ticket>()
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
private var needsToRecomputeTrackedChunks = true
private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener {
override fun onEntityAdded(entity: AbstractEntity) {}
override fun onEntityRemoved(entity: AbstractEntity) {}
private inner class ChunkListener(val pos: ChunkPos) : IChunkListener {
override fun onEntityAdded(entity: AbstractEntity) {
//if (entity is WorldObject && !isLegacy)
// send(SpawnWorldObjectPacket(entity.uuid, entity.serialize()))
}
override fun onEntityRemoved(entity: AbstractEntity) {
//send(ForgetEntityPacket(entity.uuid))
}
val dirtyCells = ObjectArraySet<Vector2i>()
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
pendingSend.add(pos)
dirtyCells.add(Vector2i(x, y))
}
}
@ -162,7 +124,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
fun onLeaveWorld() {
tasks.clear()
tickets.values.forEach { it.cancel() }
tickets.values.forEach { it.ticket.cancel() }
tickets.clear()
pendingSend.clear()
playerEntity = null
@ -210,7 +172,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
flush()
}
tickets.values.forEach { it.cancel() }
tickets.values.forEach { it.ticket.cancel() }
tickets.clear()
pendingSend.clear()
@ -226,41 +188,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
}
private fun recomputeTrackedChunks() {
val world = world ?: return
val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition)
needsToRecomputeTrackedChunks = false
// if (trackedPositionChunk == this.trackedPositionChunk) return
val tracked = ObjectOpenHashSet<ChunkPos>()
for (x in -trackedChunksWidth .. trackedChunksWidth) {
for (y in -trackedChunksHeight .. trackedChunksHeight) {
tracked.add(world.geometry.wrap(trackedPositionChunk + ChunkPos(x, y)))
}
}
val itr = tickets.entries.iterator()
for ((pos, ticket) in itr) {
if (pos !in tracked) {
send(ForgetChunkPacket(pos))
pendingSend.remove(pos)
ticket.cancel()
itr.remove()
}
}
for (pos in tracked) {
if (pos !in tickets) {
val ticket = world.permanentChunkTicket(pos)
tickets[pos] = ticket
ticket.listener = ChunkListener(pos)
pendingSend.add(pos)
}
}
}
private val entityVersions = Int2LongOpenHashMap()
init {
@ -281,22 +208,60 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
playerEntity = world.entities[playerID.get()] as? PlayerEntity
if (needsToRecomputeTrackedChunks) {
recomputeTrackedChunks()
}
run {
val newTrackedChunks = ObjectArraySet<ChunkPos>()
val itr = pendingSend.iterator()
for (pos in itr) {
val chunk = world.chunkMap[pos] ?: continue
if (isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk))
} else {
send(ChunkCellsPacket(chunk))
for (region in trackingTileRegions()) {
newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region))
}
itr.remove()
val itr = tickets.entries.iterator()
for ((pos, ticket) in itr) {
if (pos !in newTrackedChunks) {
pendingSend.remove(pos)
ticket.ticket.cancel()
itr.remove()
} else {
if (ticket.dirtyCells.isNotEmpty()) {
val chunk = world.chunkMap[ticket.pos] ?: continue
for (tilePos in ticket.dirtyCells) {
if (isLegacy) {
send(LegacyTileUpdatePacket(pos.tile + tilePos, chunk.getCell(tilePos).toLegacyNet()))
}
}
ticket.dirtyCells.clear()
}
}
}
for (pos in newTrackedChunks) {
if (pos !in tickets) {
val ticket = world.permanentChunkTicket(pos)
val thisTicket = Ticket(ticket, pos)
tickets[pos] = thisTicket
ticket.listener = thisTicket
pendingSend.add(pos)
}
}
}
run {
val itr = pendingSend.iterator()
for (pos in itr) {
val chunk = world.chunkMap[pos] ?: continue
if (isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk))
} else {
send(ChunkCellsPacket(chunk))
}
itr.remove()
}
}
for ((id, entity) in world.entities) {

View File

@ -1,21 +0,0 @@
package ru.dbotthepony.kstarbound.server.network.packets
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
data class TrackedPositionPacket(val pos: Vector2d) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVector2d())
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeStruct2d(pos)
}
override fun play(connection: ServerConnection) {
connection.trackedPosition = pos
}
}

View File

@ -1,25 +0,0 @@
package ru.dbotthepony.kstarbound.server.network.packets
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
data class TrackedSizePacket(val width: Int, val height: Int) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readUnsignedByte(), stream.readUnsignedByte())
init {
require(width in 1 .. 12) { "Bad chunk width to track: $width" }
require(height in 1 .. 12) { "Bad chunk height to track: $height" }
}
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(width)
stream.writeByte(height)
}
override fun play(connection: ServerConnection) {
connection.trackedChunksWidth = width
connection.trackedChunksHeight = height
}
}

View File

@ -61,7 +61,6 @@ class ServerWorld private constructor(
player.world?.removePlayer(player)
player.world = this
player.worldStartAcknowledged = false
player.trackedPosition = playerSpawnPosition
if (player.isLegacy) {
val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true)

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.math.divideUp
fun positiveModulo(a: Int, b: Int): Int {
@ -29,6 +30,20 @@ abstract class CoordinateMapper {
fun chunkFromCell(value: Float): Int = chunkFromCell(value.toInt())
fun chunkFromCell(value: Double): Int = chunkFromCell(value.toInt())
data class SplitResult(val rangeA: Vector2i, val rangeB: Vector2i?, val offset: Int)
/**
* One of:
* * returns provided range as-is if it is contained in this range
* * splits in two if it wraps around one of edges
* * clamps if _this_ range is contained in provided range
*/
fun split(range: IntRange): SplitResult {
return split(range.first, range.last)
}
abstract fun split(first: Int, last: Int): SplitResult
// inside world bounds
abstract fun inBoundsCell(value: Int): Boolean
abstract fun inBoundsChunk(value: Int): Boolean
@ -54,6 +69,41 @@ abstract class CoordinateMapper {
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)
override fun split(first: Int, last: Int): SplitResult {
if (first >= last) {
// point or empty range
val wrap = cell(first)
return SplitResult(Vector2i(wrap, wrap), null, 0)
} else if (first <= 0 && last >= cells) {
// covers entire world along this axis
return SplitResult(Vector2i(0, cells - 1), null, 0)
} else if (first >= 0 && last < cells) {
// within range along this axis
return SplitResult(Vector2i(first, last), null, 0)
} else {
val newFirst = cell(first)
val newLast = cell(last)
if (first < 0) {
// wrapped around left edge
return SplitResult(
Vector2i(0, newLast),
Vector2i(newFirst, cells - 1),
newFirst - cells
)
} else {
// wrapped around right edge
return SplitResult(
Vector2i(newFirst, cells - 1),
Vector2i(0, newLast),
-newLast - 1
)
}
}
}
}
class Clamper(private val cells: Int) : CoordinateMapper() {
@ -71,6 +121,11 @@ abstract class CoordinateMapper {
return value in 0 until chunks
}
override fun split(first: Int, last: Int): SplitResult {
// just clamp
return SplitResult(Vector2i(cell(first), cell(last)), null, 0)
}
override fun cell(value: Int): Int {
return value.coerceIn(0, cells - 1)
}

View File

@ -1,7 +1,10 @@
package ru.dbotthepony.kstarbound.world
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f
import ru.dbotthepony.kommons.util.IStruct2i
@ -68,4 +71,60 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Bool
if (x == pos.x && y == pos.y) return pos.toLong()
return ChunkPos.toLong(x, y)
}
// AABB + offset; offset allows to reconstruct original coordinate
// as if it weren't wrapped around world edge
fun split(region: AABBi): Pair<List<AABBi>, Vector2i> {
val splitX = x.split(region.mins.x, region.maxs.x)
val splitY = y.split(region.mins.y, region.maxs.y)
val offset = Vector2i(splitX.offset, splitY.offset)
if (splitX.rangeB == null && splitY.rangeB == null) {
// confined
return listOf(AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))) to offset
} else if (splitX.rangeB != null && splitY.rangeB == null) {
// wrapped around X axis
val a = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))
val b = AABBi(Vector2i(splitX.rangeB.x, splitY.rangeA.x), Vector2i(splitX.rangeB.y, splitY.rangeA.y))
return listOf(a, b) to offset
} else if (splitX.rangeB == null && splitY.rangeB != null) {
// wrapped around Y axis
val a = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))
val b = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeB.x), Vector2i(splitX.rangeA.y, splitY.rangeB.y))
return listOf(a, b) to offset
} else {
// wrapped around X and Y axis
splitX.rangeB!!
splitY.rangeB!!
val a = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeA.x), Vector2i(splitX.rangeA.y, splitY.rangeA.y))
val b = AABBi(Vector2i(splitX.rangeB.x, splitY.rangeA.x), Vector2i(splitX.rangeB.y, splitY.rangeA.y))
val c = AABBi(Vector2i(splitX.rangeA.x, splitY.rangeB.x), Vector2i(splitX.rangeA.y, splitY.rangeB.y))
val d = AABBi(Vector2i(splitX.rangeB.x, splitY.rangeB.x), Vector2i(splitX.rangeB.y, splitY.rangeB.y))
return listOf(a, b, c, d) to offset
}
}
fun tileRegion2Chunks(region: AABBi): Set<ChunkPos> {
if (region.mins == region.maxs)
return emptySet()
val result = ObjectArraySet<ChunkPos>()
for (actualRegion in split(region).first) {
val xMin = this.x.chunkFromCell(actualRegion.mins.x)
val xMax = this.x.chunkFromCell(actualRegion.maxs.x)
val yMin = this.y.chunkFromCell(actualRegion.mins.y)
val yMax = this.y.chunkFromCell(actualRegion.maxs.y)
for (x in xMin .. xMax) {
for (y in yMin .. yMax) {
result.add(ChunkPos(x, y))
}
}
}
return result
}
}

View File

@ -21,7 +21,7 @@ sealed class AbstractCell {
abstract fun immutable(): ImmutableCell
abstract fun mutable(): MutableCell
fun toLegacyNet(): LegacyNetworkCellState {
open fun toLegacyNet(): LegacyNetworkCellState {
return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), foreground.material.value.collisionKind, biome, envBiome, liquid.toLegacyNet(), dungeonId)
}

View File

@ -29,7 +29,7 @@ sealed class AbstractTileState {
return (modifierHueShift / 360f * 255).toInt()
}
fun toLegacyNet(): LegacyNetworkTileState {
open fun toLegacyNet(): LegacyNetworkTileState {
if (material.id != null && material.id in 0 .. 65535) {
val validMod = modifier?.id != null && modifier!!.id!! in 0 .. 65535
return LegacyNetworkTileState(material.id!!, byteHueShift(), color.ordinal, if (validMod) modifier!!.id!! else 0, if (validMod) byteModifierHueShift() else 0)

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
data class ImmutableCell(
override val foreground: ImmutableTileState = AbstractTileState.EMPTY,
override val background: ImmutableTileState = AbstractTileState.EMPTY,
@ -14,6 +16,12 @@ data class ImmutableCell(
return this
}
private val legacyNet by lazy { super.toLegacyNet() }
override fun toLegacyNet(): LegacyNetworkCellState {
return legacyNet
}
override fun mutable(): MutableCell {
return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible)
}

View File

@ -4,6 +4,7 @@ import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState
data class ImmutableTileState(
override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL,
@ -16,6 +17,12 @@ data class ImmutableTileState(
return this
}
private val legacyNet by lazy { super.toLegacyNet() }
override fun toLegacyNet(): LegacyNetworkTileState {
return legacyNet
}
override fun mutable(): MutableTileState {
return MutableTileState(material, modifier, color, hueShift, modifierHueShift)
}

View File

@ -0,0 +1,35 @@
package ru.dbotthepony.kstarbound.test
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.WorldGeometry
object WorldTests {
@Test
@DisplayName("World geometry splitting test")
fun worldGeometrySplit() {
val geometry = WorldGeometry(Vector2i(3000, 2000), true, false)
val testCases = listOf(
AABBi(Vector2i(4, 4), Vector2i(100, 100)) to listOf(AABBi(Vector2i(4, 4), Vector2i(100, 100))),
AABBi(Vector2i(0, 0), Vector2i(100, 100)) to listOf(AABBi(Vector2i(0, 0), Vector2i(100, 100))),
AABBi(Vector2i(-1, 0), Vector2i(100, 100)) to listOf(AABBi(Vector2i(0, 0), Vector2i(100, 100)), AABBi(Vector2i(2999, 0), Vector2i(2999, 100))),
AABBi(Vector2i(-3, 0), Vector2i(100, 100)) to listOf(AABBi(Vector2i(0, 0), Vector2i(100, 100)), AABBi(Vector2i(2997, 0), Vector2i(2999, 100))),
AABBi(Vector2i(-30, 0), Vector2i(100, 100)) to listOf(AABBi(Vector2i(0, 0), Vector2i(100, 100)), AABBi(Vector2i(2999 - 29, 0), Vector2i(2999, 100))),
AABBi(Vector2i(2900, 0), Vector2i(2950, 100)) to listOf(AABBi(Vector2i(2900, 0), Vector2i(2950, 100))),
AABBi(Vector2i(2900, 0), Vector2i(2999, 100)) to listOf(AABBi(Vector2i(2900, 0), Vector2i(2999, 100))),
AABBi(Vector2i(2900, 0), Vector2i(3000, 100)) to listOf(AABBi(Vector2i(2900, 0), Vector2i(2999, 100)), AABBi(Vector2i(0, 0), Vector2i(0, 100))),
AABBi(Vector2i(2900, 0), Vector2i(3004, 100)) to listOf(AABBi(Vector2i(2900, 0), Vector2i(2999, 100)), AABBi(Vector2i(0, 0), Vector2i(4, 100))),
)
for ((input, expected) in testCases) {
val split = geometry.split(input)
if (!expected.all { split.first.contains(it) }) {
throw IllegalArgumentException("Input/expected/got:\n\t$input\n\t$expected\n\t${split.first}")
}
}
}
}