ServerWorldTracker

This commit is contained in:
DBotThePony 2024-03-28 22:29:21 +07:00
parent 77f19b77ff
commit f2c2db2e9d
Signed by: DBot
GPG Key ID: DCC23B5715498507
9 changed files with 279 additions and 274 deletions

View File

@ -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.

View File

@ -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) {

View File

@ -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")

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -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) {

View File

@ -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() {
}

View File

@ -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) {