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

View File

@ -243,7 +243,7 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument
return 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) { 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.StreamCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement 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)) val clientStateGroup = MasterElement(NetworkedGroup(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientSpectatingEntities))
// in tiles // in tiles
fun trackingRegions(): List<AABBi> { fun trackingTileRegions(): List<AABBi> {
val result = ArrayList<AABBi>() val result = ArrayList<AABBi>()
val mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder) 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) { if (playerEntity != null) {
// add an extra region the size of the window centered on the player's position to prevent nearby sectors // 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 // 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()) { for (entity in clientSpectatingEntities.get()) {

View File

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

View File

@ -4,21 +4,16 @@ import com.google.gson.JsonObject
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap 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.ints.Int2ObjectMaps
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream 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.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket 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.defs.WarpAlias
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide 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.EntityCreatePacket
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket 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.PlayerWarpResultPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.server.world.WorldStorage 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.IChunkListener
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.HashMap import java.util.HashMap
@ -76,33 +71,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
return "ServerConnection[ID=$connectionID channel=$channel / $ship]" 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 shipChunks = HashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>() private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<WorldStorage>() var shipChunkSource by Delegates.notNull<WorldStorage>()
@ -123,23 +91,17 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipChunks.putAll(chunks) shipChunks.putAll(chunks)
} }
private val tickets = HashMap<ChunkPos, ServerWorld.ITicket>() private val tickets = HashMap<ChunkPos, Ticket>()
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>() 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 { val dirtyCells = ObjectArraySet<Vector2i>()
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))
}
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) { 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() { fun onLeaveWorld() {
tasks.clear() tasks.clear()
tickets.values.forEach { it.cancel() } tickets.values.forEach { it.ticket.cancel() }
tickets.clear() tickets.clear()
pendingSend.clear() pendingSend.clear()
playerEntity = null playerEntity = null
@ -210,7 +172,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
flush() flush()
} }
tickets.values.forEach { it.cancel() } tickets.values.forEach { it.ticket.cancel() }
tickets.clear() tickets.clear()
pendingSend.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() private val entityVersions = Int2LongOpenHashMap()
init { init {
@ -281,22 +208,60 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
playerEntity = world.entities[playerID.get()] as? PlayerEntity playerEntity = world.entities[playerID.get()] as? PlayerEntity
if (needsToRecomputeTrackedChunks) { run {
recomputeTrackedChunks() val newTrackedChunks = ObjectArraySet<ChunkPos>()
}
val itr = pendingSend.iterator() for (region in trackingTileRegions()) {
newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region))
for (pos in itr) {
val chunk = world.chunkMap[pos] ?: continue
if (isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk))
} else {
send(ChunkCellsPacket(chunk))
} }
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) { 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?.removePlayer(player)
player.world = this player.world = this
player.worldStartAcknowledged = false player.worldStartAcknowledged = false
player.trackedPosition = playerSpawnPosition
if (player.isLegacy) { if (player.isLegacy) {
val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true) val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true)

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world package ru.dbotthepony.kstarbound.world
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.math.divideUp import ru.dbotthepony.kstarbound.math.divideUp
fun positiveModulo(a: Int, b: Int): Int { 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: Float): Int = chunkFromCell(value.toInt())
fun chunkFromCell(value: Double): 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 // inside world bounds
abstract fun inBoundsCell(value: Int): Boolean abstract fun inBoundsCell(value: Int): Boolean
abstract fun inBoundsChunk(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: Double): Double = positiveModulo(value, cells)
override fun cell(value: Float): Float = positiveModulo(value, cells) override fun cell(value: Float): Float = positiveModulo(value, cells)
override fun chunk(value: Int): Int = positiveModulo(value, chunks) 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() { class Clamper(private val cells: Int) : CoordinateMapper() {
@ -71,6 +121,11 @@ abstract class CoordinateMapper {
return value in 0 until chunks 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 { override fun cell(value: Int): Int {
return value.coerceIn(0, cells - 1) return value.coerceIn(0, cells - 1)
} }

View File

@ -1,7 +1,10 @@
package ru.dbotthepony.kstarbound.world 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.readVector2i
import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2f import ru.dbotthepony.kommons.util.IStruct2f
import ru.dbotthepony.kommons.util.IStruct2i 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() if (x == pos.x && y == pos.y) return pos.toLong()
return ChunkPos.toLong(x, y) 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 immutable(): ImmutableCell
abstract fun mutable(): MutableCell 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) 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() return (modifierHueShift / 360f * 255).toInt()
} }
fun toLegacyNet(): LegacyNetworkTileState { open fun toLegacyNet(): LegacyNetworkTileState {
if (material.id != null && material.id in 0 .. 65535) { if (material.id != null && material.id in 0 .. 65535) {
val validMod = modifier?.id != null && modifier!!.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) 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 package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
data class ImmutableCell( data class ImmutableCell(
override val foreground: ImmutableTileState = AbstractTileState.EMPTY, override val foreground: ImmutableTileState = AbstractTileState.EMPTY,
override val background: ImmutableTileState = AbstractTileState.EMPTY, override val background: ImmutableTileState = AbstractTileState.EMPTY,
@ -14,6 +16,12 @@ data class ImmutableCell(
return this return this
} }
private val legacyNet by lazy { super.toLegacyNet() }
override fun toLegacyNet(): LegacyNetworkCellState {
return legacyNet
}
override fun mutable(): MutableCell { override fun mutable(): MutableCell {
return MutableCell(foreground.mutable(), background.mutable(), liquid.mutable(), dungeonId, biome, envBiome, isIndestructible) 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.BuiltinMetaMaterials
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.network.LegacyNetworkTileState
data class ImmutableTileState( data class ImmutableTileState(
override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL, override var material: Registry.Entry<TileDefinition> = BuiltinMetaMaterials.NULL,
@ -16,6 +17,12 @@ data class ImmutableTileState(
return this return this
} }
private val legacyNet by lazy { super.toLegacyNet() }
override fun toLegacyNet(): LegacyNetworkTileState {
return legacyNet
}
override fun mutable(): MutableTileState { override fun mutable(): MutableTileState {
return MutableTileState(material, modifier, color, hueShift, modifierHueShift) 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}")
}
}
}
}