ServerWorldTracker
This commit is contained in:
parent
77f19b77ff
commit
f2c2db2e9d
13
SECURITY.md
13
SECURITY.md
@ -53,3 +53,16 @@ So client can forge custom shipworld, with 2^31 x 2^31 dimensions, which will in
|
||||
server to consume at least 128 GiB of RAM when client connects.
|
||||
|
||||
This attack does not require modified game client.
|
||||
|
||||
-----------
|
||||
|
||||
## Exploits in original engine
|
||||
|
||||
These kind of bugs don't directly compromise security of server, but may degrade its performance.
|
||||
|
||||
-----------
|
||||
|
||||
### Client context window size
|
||||
|
||||
Window size as reported by client is not checked for insane values, allowing to greatly slowdown the server if client
|
||||
is residing in large world, and reporting to be tracking entire world.
|
||||
|
@ -157,7 +157,6 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
|
||||
val playerID = networkedSignedInt()
|
||||
|
||||
var playerEntity: PlayerEntity? = null
|
||||
protected set
|
||||
|
||||
// holy shit
|
||||
val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() })
|
||||
@ -167,8 +166,23 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
|
||||
fun trackingTileRegions(): List<AABBi> {
|
||||
val result = ArrayList<AABBi>()
|
||||
|
||||
val mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder)
|
||||
val maxs = Vector2i(windowWidth.get() + GlobalDefaults.client.windowMonitoringBorder, windowHeight.get() + GlobalDefaults.client.windowMonitoringBorder)
|
||||
var mins = Vector2i(windowXMin.get() - GlobalDefaults.client.windowMonitoringBorder, windowYMin.get() - GlobalDefaults.client.windowMonitoringBorder)
|
||||
var maxs = Vector2i(windowWidth.get() + GlobalDefaults.client.windowMonitoringBorder, windowHeight.get() + GlobalDefaults.client.windowMonitoringBorder)
|
||||
|
||||
if (maxs.x - mins.x > 1000) {
|
||||
// holy shit
|
||||
val middle = (maxs.x - mins.x) / 2
|
||||
mins = mins.copy(x = middle - 500)
|
||||
maxs = maxs.copy(x = middle + 500)
|
||||
}
|
||||
|
||||
if (maxs.y - mins.y > 1000) {
|
||||
// holy shit
|
||||
val middle = (maxs.y - mins.y) / 2
|
||||
mins = mins.copy(y = middle - 500)
|
||||
maxs = maxs.copy(y = middle + 500)
|
||||
}
|
||||
|
||||
val window = AABBi(mins, maxs + mins)
|
||||
|
||||
if (window.mins != Vector2i.ZERO && window.maxs != Vector2i.ZERO) {
|
||||
|
@ -3,62 +3,38 @@ package ru.dbotthepony.kstarbound.server
|
||||
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.Int2ObjectMaps
|
||||
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
|
||||
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.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
|
||||
import ru.dbotthepony.kstarbound.defs.WarpAlias
|
||||
import ru.dbotthepony.kstarbound.network.Connection
|
||||
import ru.dbotthepony.kstarbound.network.ConnectionSide
|
||||
import ru.dbotthepony.kstarbound.network.ConnectionType
|
||||
import ru.dbotthepony.kstarbound.network.IServerPacket
|
||||
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.network.packets.clientbound.TileDamageUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.server.world.ServerWorldTracker
|
||||
import ru.dbotthepony.kstarbound.server.world.WorldStorage
|
||||
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
|
||||
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.IChunkListener
|
||||
import ru.dbotthepony.kstarbound.world.TileHealth
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
||||
import java.io.DataOutputStream
|
||||
import java.util.HashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
// serverside part of connection
|
||||
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) {
|
||||
var world: ServerWorld? = null
|
||||
var tracker: ServerWorldTracker? = null
|
||||
var worldStartAcknowledged = false
|
||||
|
||||
private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
|
||||
|
||||
// packets which interact with world must be
|
||||
// executed on world's thread
|
||||
fun enqueue(task: ServerWorld.() -> Unit) {
|
||||
tasks.add(task)
|
||||
}
|
||||
fun enqueue(task: ServerWorld.() -> Unit) = tracker?.enqueue(task)
|
||||
|
||||
lateinit var shipWorld: ServerWorld
|
||||
private set
|
||||
|
||||
var skyVersion = 0L
|
||||
|
||||
init {
|
||||
connectionID = server.channels.nextConnectionID()
|
||||
|
||||
@ -93,25 +69,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
shipChunks.putAll(chunks)
|
||||
}
|
||||
|
||||
private val tickets = HashMap<ChunkPos, Ticket>()
|
||||
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
|
||||
|
||||
private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener {
|
||||
override fun onEntityAdded(entity: AbstractEntity) {}
|
||||
override fun onEntityRemoved(entity: AbstractEntity) {}
|
||||
|
||||
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
|
||||
if (pos !in pendingSend) {
|
||||
send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
|
||||
// let's hope nothing bad happens from referencing live data
|
||||
send(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, isBackground, health))
|
||||
}
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
if (isConnected) {
|
||||
val entries = rpc.write()
|
||||
@ -129,14 +86,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
super.flush()
|
||||
}
|
||||
|
||||
fun onLeaveWorld() {
|
||||
tasks.clear()
|
||||
tickets.values.forEach { it.ticket.cancel() }
|
||||
tickets.clear()
|
||||
pendingSend.clear()
|
||||
playerEntity = null
|
||||
}
|
||||
|
||||
override fun onChannelClosed() {
|
||||
playerEntity = null
|
||||
|
||||
@ -179,12 +128,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
flush()
|
||||
}
|
||||
|
||||
world?.removePlayer(this)
|
||||
world = null
|
||||
tickets.values.forEach { it.ticket.cancel() }
|
||||
tickets.clear()
|
||||
tasks.clear()
|
||||
pendingSend.clear()
|
||||
tracker?.remove()
|
||||
tracker = null
|
||||
|
||||
if (::shipWorld.isInitialized) {
|
||||
shipWorld.close()
|
||||
@ -198,100 +143,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
}
|
||||
}
|
||||
|
||||
private val entityVersions = Int2LongOpenHashMap()
|
||||
|
||||
init {
|
||||
entityVersions.defaultReturnValue(-1L)
|
||||
}
|
||||
|
||||
fun isTracking(pos: ChunkPos): Boolean {
|
||||
return pos in tickets
|
||||
}
|
||||
|
||||
fun tickWorld() {
|
||||
val world = world!!
|
||||
|
||||
run {
|
||||
var next = tasks.poll()
|
||||
|
||||
while (next != null) {
|
||||
next.invoke(world)
|
||||
next = tasks.poll()
|
||||
}
|
||||
}
|
||||
|
||||
playerEntity = world.entities[playerID.get()] as? PlayerEntity
|
||||
|
||||
run {
|
||||
val newTrackedChunks = ObjectArraySet<ChunkPos>()
|
||||
|
||||
for (region in trackingTileRegions()) {
|
||||
newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region))
|
||||
}
|
||||
|
||||
val itr = tickets.entries.iterator()
|
||||
|
||||
for ((pos, ticket) in itr) {
|
||||
if (pos !in newTrackedChunks) {
|
||||
pendingSend.remove(pos)
|
||||
ticket.ticket.cancel()
|
||||
itr.remove()
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
chunk.tileDamagePackets().forEach { send(it) }
|
||||
} else {
|
||||
send(ChunkCellsPacket(chunk))
|
||||
}
|
||||
|
||||
itr.remove()
|
||||
}
|
||||
}
|
||||
|
||||
for ((id, entity) in world.entities) {
|
||||
if (entity.connectionID != connectionID && entity is PlayerEntity) {
|
||||
if (entityVersions.get(id) == -1L) {
|
||||
// never networked
|
||||
val initial = FastByteArrayOutputStream()
|
||||
entity.writeNetwork(DataOutputStream(initial), isLegacy)
|
||||
val (data, version) = entity.networkGroup.write(isLegacy = isLegacy)
|
||||
|
||||
entityVersions.put(id, version)
|
||||
|
||||
send(EntityCreatePacket(
|
||||
entity.type,
|
||||
ByteArrayList.wrap(initial.array, initial.length),
|
||||
data,
|
||||
entity.entityID
|
||||
))
|
||||
} else {
|
||||
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = isLegacy)
|
||||
entityVersions.put(id, version)
|
||||
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg is IServerPacket) {
|
||||
try {
|
||||
@ -323,22 +174,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
||||
shipWorld.thread.start()
|
||||
send(PlayerWarpResultPacket(true, WarpAlias.OwnShip, false))
|
||||
|
||||
server.worlds.first().acceptPlayer(this)
|
||||
//server.worlds.first().acceptPlayer(this)
|
||||
|
||||
/*shipWorld.acceptPlayer(this).thenAccept {
|
||||
for (conn in server.channels.connections) {
|
||||
if (conn.isLegacy && conn !== this) {
|
||||
conn.shipWorld.acceptPlayer(this)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
server.worlds.first().acceptPlayer(this)
|
||||
}.exceptionally {
|
||||
shipWorld.acceptClient(this).exceptionally {
|
||||
LOGGER.error("Shipworld of $this rejected to accept its owner", it)
|
||||
disconnect("Shipworld rejected player warp request: $it")
|
||||
null
|
||||
}*/
|
||||
}
|
||||
}.exceptionally {
|
||||
LOGGER.error("Error while initializing shipworld for $this", it)
|
||||
disconnect("Error while initializing shipworld for player: $it")
|
||||
|
@ -13,8 +13,8 @@ import ru.dbotthepony.kstarbound.util.ExceptionLogger
|
||||
import ru.dbotthepony.kstarbound.util.ExecutionSpinner
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.util.Collections
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@ -28,8 +28,7 @@ sealed class StarboundServer(val root: File) : Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
val worlds: MutableList<ServerWorld> = Collections.synchronizedList(ArrayList<ServerWorld>())
|
||||
|
||||
val worlds = CopyOnWriteArrayList<ServerWorld>()
|
||||
val serverID = threadCounter.getAndIncrement()
|
||||
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
|
||||
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
|
||||
@ -89,7 +88,7 @@ sealed class StarboundServer(val root: File) : Closeable {
|
||||
|
||||
fun playerInGame(player: ServerConnection) {
|
||||
val world = worlds.first()
|
||||
world.acceptPlayer(player)
|
||||
world.acceptClient(player)
|
||||
}
|
||||
|
||||
protected abstract fun close0()
|
||||
|
@ -9,7 +9,6 @@ import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.util.IStruct2i
|
||||
import ru.dbotthepony.kommons.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
|
||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
||||
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
|
||||
@ -17,8 +16,6 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.network.IPacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
|
||||
import ru.dbotthepony.kstarbound.server.StarboundServer
|
||||
import ru.dbotthepony.kstarbound.server.ServerConnection
|
||||
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
||||
@ -30,7 +27,6 @@ import ru.dbotthepony.kstarbound.world.World
|
||||
import ru.dbotthepony.kstarbound.world.WorldGeometry
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
@ -54,52 +50,22 @@ class ServerWorld private constructor(
|
||||
server.worlds.add(this)
|
||||
}
|
||||
|
||||
private val internalPlayers = CopyOnWriteArrayList<ServerConnection>()
|
||||
val players: List<ServerConnection> = Collections.unmodifiableList(internalPlayers)
|
||||
val players = CopyOnWriteArrayList<ServerWorldTracker>()
|
||||
|
||||
private fun doAcceptPlayer(player: ServerConnection) {
|
||||
if (player in internalPlayers)
|
||||
throw IllegalStateException("$player is already in $this")
|
||||
private fun doAcceptClient(client: ServerConnection) {
|
||||
if (players.any { it.client == client })
|
||||
throw IllegalStateException("$client is already in $this")
|
||||
|
||||
internalPlayers.add(player)
|
||||
player.onLeaveWorld()
|
||||
player.world?.removePlayer(player)
|
||||
player.world = this
|
||||
player.worldStartAcknowledged = false
|
||||
|
||||
if (player.isLegacy) {
|
||||
val (skyData, skyVersion) = sky.networkedGroup.write(isLegacy = true)
|
||||
player.skyVersion = skyVersion
|
||||
|
||||
player.send(WorldStartPacket(
|
||||
templateData = Starbound.writeLegacyJson { template.toJson() },
|
||||
skyData = skyData.toByteArray(),
|
||||
weatherData = ByteArray(0),
|
||||
playerStart = playerSpawnPosition,
|
||||
playerRespawn = playerSpawnPosition,
|
||||
respawnInWorld = respawnInWorld,
|
||||
dungeonGravity = mapOf(),
|
||||
dungeonBreathable = mapOf(),
|
||||
protectedDungeonIDs = protectedDungeonIDs,
|
||||
worldProperties = properties.deepCopy(),
|
||||
connectionID = player.connectionID,
|
||||
localInterpolationMode = false,
|
||||
))
|
||||
|
||||
Starbound.writeLegacyJson {
|
||||
player.sendAndFlush(CentralStructureUpdatePacket(Starbound.gson.toJsonTree(centralStructure)))
|
||||
}
|
||||
} else {
|
||||
player.sendAndFlush(JoinWorldPacket(this))
|
||||
}
|
||||
client.tracker?.remove()
|
||||
players.add(ServerWorldTracker(this, client))
|
||||
}
|
||||
|
||||
fun acceptPlayer(player: ServerConnection): CompletableFuture<Unit> {
|
||||
fun acceptClient(player: ServerConnection): CompletableFuture<Unit> {
|
||||
check(!isClosed.get()) { "$this is invalid" }
|
||||
unpause()
|
||||
|
||||
try {
|
||||
return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox).exceptionally {
|
||||
return CompletableFuture.supplyAsync(Supplier { doAcceptClient(player) }, mailbox).exceptionally {
|
||||
LOGGER.error("Error while accepting new player into world", it)
|
||||
}
|
||||
} catch (err: RejectedExecutionException) {
|
||||
@ -107,35 +73,6 @@ class ServerWorld private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun doRemovePlayer(player: ServerConnection): Boolean {
|
||||
if (internalPlayers.remove(player)) {
|
||||
val itr = entities.int2ObjectEntrySet().iterator()
|
||||
|
||||
for ((id, entity) in itr) {
|
||||
if (id in player.entityIDRange) {
|
||||
entity.remove()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun removePlayer(player: ServerConnection): CompletableFuture<Boolean> {
|
||||
check(!isClosed.get()) { "$this is invalid" }
|
||||
|
||||
try {
|
||||
return CompletableFuture.supplyAsync(Supplier { doRemovePlayer(player) }, mailbox).exceptionally {
|
||||
LOGGER.error("Error while removing player from world", it)
|
||||
null
|
||||
}
|
||||
} catch (err: RejectedExecutionException) {
|
||||
return CompletableFuture.completedFuture(false)
|
||||
}
|
||||
}
|
||||
|
||||
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
|
||||
val thread = Thread(spinner, "Starbound Server World Thread")
|
||||
val ticketListLock = ReentrantLock()
|
||||
@ -160,13 +97,7 @@ class ServerWorld private constructor(
|
||||
|
||||
super.close()
|
||||
spinner.unpause()
|
||||
|
||||
lock.withLock {
|
||||
internalPlayers.forEach {
|
||||
it.world = null
|
||||
}
|
||||
}
|
||||
|
||||
players.forEach { it.remove() }
|
||||
server.worlds.remove(this)
|
||||
LockSupport.unpark(thread)
|
||||
}
|
||||
@ -210,12 +141,12 @@ class ServerWorld private constructor(
|
||||
override fun tickInner() {
|
||||
val packet = StepUpdatePacket(ticks)
|
||||
|
||||
internalPlayers.forEach {
|
||||
if (!isClosed.get() && it.worldStartAcknowledged && it.channel.isOpen) {
|
||||
players.forEach {
|
||||
if (!isClosed.get()) {
|
||||
it.send(packet)
|
||||
|
||||
try {
|
||||
it.tickWorld()
|
||||
it.tick()
|
||||
} catch (err: Throwable) {
|
||||
LOGGER.error("Exception while ticking player $it", err)
|
||||
//it.disconnect("Exception while ticking player: $err")
|
||||
@ -253,7 +184,7 @@ class ServerWorld private constructor(
|
||||
}
|
||||
|
||||
override fun broadcast(packet: IPacket) {
|
||||
internalPlayers.forEach {
|
||||
players.forEach {
|
||||
it.send(packet)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,214 @@
|
||||
package ru.dbotthepony.kstarbound.server.world
|
||||
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrayList
|
||||
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps
|
||||
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
|
||||
import ru.dbotthepony.kstarbound.network.IPacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.EntityCreatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.EntityUpdateSetPacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.CentralStructureUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
|
||||
import ru.dbotthepony.kstarbound.server.ServerConnection
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.IChunkListener
|
||||
import ru.dbotthepony.kstarbound.world.TileHealth
|
||||
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
||||
import java.io.DataOutputStream
|
||||
import java.util.HashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
// couples ServerWorld and ServerConnection together,
|
||||
// allowing ServerConnection client to track ServerWorld state
|
||||
class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection) {
|
||||
init {
|
||||
client.worldStartAcknowledged = false
|
||||
client.tracker = this
|
||||
}
|
||||
|
||||
var skyVersion = 0L
|
||||
|
||||
private val isRemoved = AtomicBoolean()
|
||||
private val tickets = HashMap<ChunkPos, Ticket>()
|
||||
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
|
||||
private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
|
||||
private val entityVersions = Int2LongOpenHashMap()
|
||||
|
||||
init {
|
||||
entityVersions.defaultReturnValue(-1L)
|
||||
}
|
||||
|
||||
fun send(packet: IPacket) = client.send(packet)
|
||||
|
||||
// packets which interact with world must be
|
||||
// executed on world's thread
|
||||
fun enqueue(task: ServerWorld.() -> Unit) {
|
||||
tasks.add(task)
|
||||
}
|
||||
|
||||
private inner class Ticket(val ticket: ServerWorld.ITicket, val pos: ChunkPos) : IChunkListener {
|
||||
override fun onEntityAdded(entity: AbstractEntity) {}
|
||||
override fun onEntityRemoved(entity: AbstractEntity) {}
|
||||
|
||||
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {
|
||||
if (pos !in pendingSend) {
|
||||
send(LegacyTileUpdatePacket(pos.tile + Vector2i(x, y), cell.toLegacyNet()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTileHealthUpdate(x: Int, y: Int, isBackground: Boolean, health: TileHealth) {
|
||||
// let's hope nothing bad happens from referencing live data
|
||||
send(TileDamageUpdatePacket(pos.tileX + x, pos.tileY + y, isBackground, health))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val (skyData, skyVersion) = world.sky.networkedGroup.write(isLegacy = client.isLegacy)
|
||||
this.skyVersion = skyVersion
|
||||
|
||||
if (client.isLegacy) {
|
||||
client.send(WorldStartPacket(
|
||||
templateData = Starbound.writeLegacyJson { world.template.toJson() },
|
||||
skyData = skyData.toByteArray(),
|
||||
weatherData = ByteArray(0),
|
||||
playerStart = world.playerSpawnPosition,
|
||||
playerRespawn = world.playerSpawnPosition,
|
||||
respawnInWorld = world.respawnInWorld,
|
||||
dungeonGravity = mapOf(),
|
||||
dungeonBreathable = mapOf(),
|
||||
protectedDungeonIDs = world.protectedDungeonIDs,
|
||||
worldProperties = world.properties.deepCopy(),
|
||||
connectionID = client.connectionID,
|
||||
localInterpolationMode = false,
|
||||
))
|
||||
|
||||
Starbound.writeLegacyJson {
|
||||
client.sendAndFlush(CentralStructureUpdatePacket(Starbound.gson.toJsonTree(world.centralStructure)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isTracking(pos: ChunkPos): Boolean {
|
||||
return pos in tickets
|
||||
}
|
||||
|
||||
fun tick() {
|
||||
if (!client.worldStartAcknowledged)
|
||||
return
|
||||
|
||||
if (!client.channel.isOpen) {
|
||||
remove() // ???
|
||||
return
|
||||
}
|
||||
|
||||
run {
|
||||
var next = tasks.poll()
|
||||
|
||||
while (next != null) {
|
||||
next.invoke(world)
|
||||
next = tasks.poll()
|
||||
}
|
||||
}
|
||||
|
||||
client.playerEntity = world.entities[client.playerID.get()] as? PlayerEntity
|
||||
|
||||
run {
|
||||
val newTrackedChunks = ObjectArraySet<ChunkPos>()
|
||||
|
||||
for (region in client.trackingTileRegions()) {
|
||||
newTrackedChunks.addAll(world.geometry.tileRegion2Chunks(region))
|
||||
}
|
||||
|
||||
val itr = tickets.entries.iterator()
|
||||
|
||||
for ((pos, ticket) in itr) {
|
||||
if (pos !in newTrackedChunks) {
|
||||
pendingSend.remove(pos)
|
||||
ticket.ticket.cancel()
|
||||
itr.remove()
|
||||
}
|
||||
}
|
||||
|
||||
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 (client.isLegacy) {
|
||||
send(LegacyTileArrayUpdatePacket(chunk))
|
||||
chunk.tileDamagePackets().forEach { send(it) }
|
||||
} else {
|
||||
send(ChunkCellsPacket(chunk))
|
||||
}
|
||||
|
||||
itr.remove()
|
||||
}
|
||||
}
|
||||
|
||||
for ((id, entity) in world.entities) {
|
||||
if (entity.connectionID != client.connectionID && entity is PlayerEntity) {
|
||||
if (entityVersions.get(id) == -1L) {
|
||||
// never networked
|
||||
val initial = FastByteArrayOutputStream()
|
||||
entity.writeNetwork(DataOutputStream(initial), client.isLegacy)
|
||||
val (data, version) = entity.networkGroup.write(isLegacy = client.isLegacy)
|
||||
|
||||
entityVersions.put(id, version)
|
||||
|
||||
send(EntityCreatePacket(
|
||||
entity.type,
|
||||
ByteArrayList.wrap(initial.array, initial.length),
|
||||
data,
|
||||
entity.entityID
|
||||
))
|
||||
} else {
|
||||
val (data, version) = entity.networkGroup.write(remoteVersion = entityVersions.get(id), isLegacy = client.isLegacy)
|
||||
entityVersions.put(id, version)
|
||||
send(EntityUpdateSetPacket(entity.connectionID, Int2ObjectMaps.singleton(entity.entityID, data)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
if (isRemoved.compareAndSet(false, true)) {
|
||||
client.tracker = null
|
||||
client.playerEntity = null
|
||||
world.players.remove(this)
|
||||
tickets.values.forEach { it.ticket.cancel() }
|
||||
|
||||
world.mailbox.execute {
|
||||
val itr = world.entities.int2ObjectEntrySet().iterator()
|
||||
|
||||
for ((id, entity) in itr) {
|
||||
if (id in client.entityIDRange) {
|
||||
entity.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,6 @@ import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
|
||||
import ru.dbotthepony.kstarbound.math.*
|
||||
import ru.dbotthepony.kstarbound.network.IPacket
|
||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
|
||||
import ru.dbotthepony.kstarbound.server.StarboundServer
|
||||
import ru.dbotthepony.kstarbound.util.ExceptionLogger
|
||||
import ru.dbotthepony.kstarbound.util.ParallelPerform
|
||||
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
||||
@ -35,7 +34,6 @@ import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
|
||||
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
|
||||
import java.io.Closeable
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.function.Predicate
|
||||
@ -276,7 +274,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), { if (!it.isRemote) it.movement.move() })).join()
|
||||
mailbox.executeQueuedTasks()
|
||||
|
||||
entities.values.forEach { it.think() }
|
||||
entities.values.forEach { it.tick() }
|
||||
mailbox.executeQueuedTasks()
|
||||
|
||||
for (chunk in chunkMap) {
|
||||
|
@ -12,13 +12,10 @@ import ru.dbotthepony.kstarbound.defs.JsonDriven
|
||||
import ru.dbotthepony.kstarbound.network.packets.EntityDestroyPacket
|
||||
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
|
||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
|
||||
import ru.dbotthepony.kstarbound.server.StarboundServer
|
||||
import ru.dbotthepony.kstarbound.util.ExceptionLogger
|
||||
import ru.dbotthepony.kstarbound.world.Chunk
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.LightCalculator
|
||||
import ru.dbotthepony.kstarbound.world.World
|
||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
||||
import java.io.DataOutputStream
|
||||
import java.util.function.Consumer
|
||||
import kotlin.concurrent.withLock
|
||||
@ -148,25 +145,25 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
|
||||
|
||||
var isRemote: Boolean = false
|
||||
|
||||
fun think() {
|
||||
thinkShared()
|
||||
fun tick() {
|
||||
tickShared()
|
||||
|
||||
if (isRemote) {
|
||||
thinkRemote()
|
||||
tickRemote()
|
||||
} else {
|
||||
thinkLocal()
|
||||
tickLocal()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun thinkShared() {
|
||||
protected open fun tickShared() {
|
||||
mailbox.executeQueuedTasks()
|
||||
}
|
||||
|
||||
protected open fun thinkRemote() {
|
||||
protected open fun tickRemote() {
|
||||
networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP)
|
||||
}
|
||||
|
||||
protected open fun thinkLocal() {
|
||||
protected open fun tickLocal() {
|
||||
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,6 @@ import com.google.common.collect.ImmutableMap
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import it.unimi.dsi.fastutil.bytes.ByteArrayList
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
|
||||
import ru.dbotthepony.kommons.math.RGBAColor
|
||||
import ru.dbotthepony.kommons.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
@ -139,12 +137,12 @@ open class WorldObject(
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
override fun thinkShared() {
|
||||
super.thinkShared()
|
||||
override fun tickShared() {
|
||||
super.tickShared()
|
||||
flickerPeriod?.update(Starbound.TIMESTEP, world.random)
|
||||
}
|
||||
|
||||
override fun thinkRemote() {
|
||||
override fun tickRemote() {
|
||||
val orientation = orientation
|
||||
|
||||
if (orientation != null) {
|
||||
|
Loading…
Reference in New Issue
Block a user