Proper chunk tracking
This commit is contained in:
parent
82b09721fc
commit
a5192bc551
src
main/kotlin/ru/dbotthepony/kstarbound
client
lua
network
server
world
test/kotlin/ru/dbotthepony/kstarbound/test
@ -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 }
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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()) {
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
35
src/test/kotlin/ru/dbotthepony/kstarbound/test/WorldTests.kt
Normal file
35
src/test/kotlin/ru/dbotthepony/kstarbound/test/WorldTests.kt
Normal 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user