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.
|
server to consume at least 128 GiB of RAM when client connects.
|
||||||
|
|
||||||
This attack does not require modified game client.
|
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()
|
val playerID = networkedSignedInt()
|
||||||
|
|
||||||
var playerEntity: PlayerEntity? = null
|
var playerEntity: PlayerEntity? = null
|
||||||
protected set
|
|
||||||
|
|
||||||
// holy shit
|
// holy shit
|
||||||
val clientSpectatingEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() })
|
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> {
|
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)
|
var 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 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)
|
val window = AABBi(mins, maxs + mins)
|
||||||
|
|
||||||
if (window.mins != Vector2i.ZERO && window.maxs != Vector2i.ZERO) {
|
if (window.mins != Vector2i.ZERO && window.maxs != Vector2i.ZERO) {
|
||||||
|
@ -3,62 +3,38 @@ package ru.dbotthepony.kstarbound.server
|
|||||||
import com.google.gson.JsonObject
|
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.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 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.vector.Vector2i
|
|
||||||
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
|
|
||||||
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
|
||||||
import ru.dbotthepony.kstarbound.network.ConnectionType
|
import ru.dbotthepony.kstarbound.network.ConnectionType
|
||||||
import ru.dbotthepony.kstarbound.network.IServerPacket
|
import ru.dbotthepony.kstarbound.network.IServerPacket
|
||||||
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
|
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.PlayerWarpResultPacket
|
||||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
|
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.WorldStorage
|
||||||
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
|
import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
|
||||||
import ru.dbotthepony.kstarbound.server.world.ServerWorld
|
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.HashMap
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
// serverside part of connection
|
// serverside part of connection
|
||||||
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) {
|
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) {
|
||||||
var world: ServerWorld? = null
|
var world: ServerWorld? = null
|
||||||
|
var tracker: ServerWorldTracker? = null
|
||||||
var worldStartAcknowledged = false
|
var worldStartAcknowledged = false
|
||||||
|
|
||||||
private val tasks = ConcurrentLinkedQueue<ServerWorld.() -> Unit>()
|
|
||||||
|
|
||||||
// packets which interact with world must be
|
// packets which interact with world must be
|
||||||
// executed on world's thread
|
// executed on world's thread
|
||||||
fun enqueue(task: ServerWorld.() -> Unit) {
|
fun enqueue(task: ServerWorld.() -> Unit) = tracker?.enqueue(task)
|
||||||
tasks.add(task)
|
|
||||||
}
|
|
||||||
|
|
||||||
lateinit var shipWorld: ServerWorld
|
lateinit var shipWorld: ServerWorld
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var skyVersion = 0L
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
connectionID = server.channels.nextConnectionID()
|
connectionID = server.channels.nextConnectionID()
|
||||||
|
|
||||||
@ -93,25 +69,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
|||||||
shipChunks.putAll(chunks)
|
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() {
|
override fun flush() {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
val entries = rpc.write()
|
val entries = rpc.write()
|
||||||
@ -129,14 +86,6 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
|||||||
super.flush()
|
super.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLeaveWorld() {
|
|
||||||
tasks.clear()
|
|
||||||
tickets.values.forEach { it.ticket.cancel() }
|
|
||||||
tickets.clear()
|
|
||||||
pendingSend.clear()
|
|
||||||
playerEntity = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChannelClosed() {
|
override fun onChannelClosed() {
|
||||||
playerEntity = null
|
playerEntity = null
|
||||||
|
|
||||||
@ -179,12 +128,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
|||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
world?.removePlayer(this)
|
tracker?.remove()
|
||||||
world = null
|
tracker = null
|
||||||
tickets.values.forEach { it.ticket.cancel() }
|
|
||||||
tickets.clear()
|
|
||||||
tasks.clear()
|
|
||||||
pendingSend.clear()
|
|
||||||
|
|
||||||
if (::shipWorld.isInitialized) {
|
if (::shipWorld.isInitialized) {
|
||||||
shipWorld.close()
|
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) {
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
if (msg is IServerPacket) {
|
if (msg is IServerPacket) {
|
||||||
try {
|
try {
|
||||||
@ -323,22 +174,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
|
|||||||
shipWorld.thread.start()
|
shipWorld.thread.start()
|
||||||
send(PlayerWarpResultPacket(true, WarpAlias.OwnShip, false))
|
send(PlayerWarpResultPacket(true, WarpAlias.OwnShip, false))
|
||||||
|
|
||||||
server.worlds.first().acceptPlayer(this)
|
//server.worlds.first().acceptPlayer(this)
|
||||||
|
|
||||||
/*shipWorld.acceptPlayer(this).thenAccept {
|
shipWorld.acceptClient(this).exceptionally {
|
||||||
for (conn in server.channels.connections) {
|
|
||||||
if (conn.isLegacy && conn !== this) {
|
|
||||||
conn.shipWorld.acceptPlayer(this)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.worlds.first().acceptPlayer(this)
|
|
||||||
}.exceptionally {
|
|
||||||
LOGGER.error("Shipworld of $this rejected to accept its owner", it)
|
LOGGER.error("Shipworld of $this rejected to accept its owner", it)
|
||||||
disconnect("Shipworld rejected player warp request: $it")
|
disconnect("Shipworld rejected player warp request: $it")
|
||||||
null
|
}
|
||||||
}*/
|
|
||||||
}.exceptionally {
|
}.exceptionally {
|
||||||
LOGGER.error("Error while initializing shipworld for $this", it)
|
LOGGER.error("Error while initializing shipworld for $this", it)
|
||||||
disconnect("Error while initializing shipworld for player: $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 ru.dbotthepony.kstarbound.util.ExecutionSpinner
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.Collections
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
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 serverID = threadCounter.getAndIncrement()
|
||||||
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
|
val mailbox = MailboxExecutorService().also { it.exceptionHandler = ExceptionLogger(LOGGER) }
|
||||||
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
|
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
|
||||||
@ -89,7 +88,7 @@ sealed class StarboundServer(val root: File) : Closeable {
|
|||||||
|
|
||||||
fun playerInGame(player: ServerConnection) {
|
fun playerInGame(player: ServerConnection) {
|
||||||
val world = worlds.first()
|
val world = worlds.first()
|
||||||
world.acceptPlayer(player)
|
world.acceptClient(player)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun close0()
|
protected abstract fun close0()
|
||||||
|
@ -9,7 +9,6 @@ import org.apache.logging.log4j.LogManager
|
|||||||
import ru.dbotthepony.kommons.util.IStruct2i
|
import ru.dbotthepony.kommons.util.IStruct2i
|
||||||
import ru.dbotthepony.kommons.vector.Vector2d
|
import ru.dbotthepony.kommons.vector.Vector2d
|
||||||
import ru.dbotthepony.kstarbound.Starbound
|
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.TileDamage
|
||||||
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
import ru.dbotthepony.kstarbound.defs.tile.TileDamageResult
|
||||||
import ru.dbotthepony.kstarbound.defs.world.WorldStructure
|
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.json.builder.JsonFactory
|
||||||
import ru.dbotthepony.kstarbound.network.IPacket
|
import ru.dbotthepony.kstarbound.network.IPacket
|
||||||
import ru.dbotthepony.kstarbound.network.packets.StepUpdatePacket
|
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.StarboundServer
|
||||||
import ru.dbotthepony.kstarbound.server.ServerConnection
|
import ru.dbotthepony.kstarbound.server.ServerConnection
|
||||||
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
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.WorldGeometry
|
||||||
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 java.util.Collections
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.RejectedExecutionException
|
import java.util.concurrent.RejectedExecutionException
|
||||||
@ -54,52 +50,22 @@ class ServerWorld private constructor(
|
|||||||
server.worlds.add(this)
|
server.worlds.add(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val internalPlayers = CopyOnWriteArrayList<ServerConnection>()
|
val players = CopyOnWriteArrayList<ServerWorldTracker>()
|
||||||
val players: List<ServerConnection> = Collections.unmodifiableList(internalPlayers)
|
|
||||||
|
|
||||||
private fun doAcceptPlayer(player: ServerConnection) {
|
private fun doAcceptClient(client: ServerConnection) {
|
||||||
if (player in internalPlayers)
|
if (players.any { it.client == client })
|
||||||
throw IllegalStateException("$player is already in $this")
|
throw IllegalStateException("$client is already in $this")
|
||||||
|
|
||||||
internalPlayers.add(player)
|
client.tracker?.remove()
|
||||||
player.onLeaveWorld()
|
players.add(ServerWorldTracker(this, client))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun acceptPlayer(player: ServerConnection): CompletableFuture<Unit> {
|
fun acceptClient(player: ServerConnection): CompletableFuture<Unit> {
|
||||||
check(!isClosed.get()) { "$this is invalid" }
|
check(!isClosed.get()) { "$this is invalid" }
|
||||||
unpause()
|
unpause()
|
||||||
|
|
||||||
try {
|
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)
|
LOGGER.error("Error while accepting new player into world", it)
|
||||||
}
|
}
|
||||||
} catch (err: RejectedExecutionException) {
|
} 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 spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TIMESTEP_NANOS)
|
||||||
val thread = Thread(spinner, "Starbound Server World Thread")
|
val thread = Thread(spinner, "Starbound Server World Thread")
|
||||||
val ticketListLock = ReentrantLock()
|
val ticketListLock = ReentrantLock()
|
||||||
@ -160,13 +97,7 @@ class ServerWorld private constructor(
|
|||||||
|
|
||||||
super.close()
|
super.close()
|
||||||
spinner.unpause()
|
spinner.unpause()
|
||||||
|
players.forEach { it.remove() }
|
||||||
lock.withLock {
|
|
||||||
internalPlayers.forEach {
|
|
||||||
it.world = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.worlds.remove(this)
|
server.worlds.remove(this)
|
||||||
LockSupport.unpark(thread)
|
LockSupport.unpark(thread)
|
||||||
}
|
}
|
||||||
@ -210,12 +141,12 @@ class ServerWorld private constructor(
|
|||||||
override fun tickInner() {
|
override fun tickInner() {
|
||||||
val packet = StepUpdatePacket(ticks)
|
val packet = StepUpdatePacket(ticks)
|
||||||
|
|
||||||
internalPlayers.forEach {
|
players.forEach {
|
||||||
if (!isClosed.get() && it.worldStartAcknowledged && it.channel.isOpen) {
|
if (!isClosed.get()) {
|
||||||
it.send(packet)
|
it.send(packet)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
it.tickWorld()
|
it.tick()
|
||||||
} catch (err: Throwable) {
|
} catch (err: Throwable) {
|
||||||
LOGGER.error("Exception while ticking player $it", err)
|
LOGGER.error("Exception while ticking player $it", err)
|
||||||
//it.disconnect("Exception while ticking player: $err")
|
//it.disconnect("Exception while ticking player: $err")
|
||||||
@ -253,7 +184,7 @@ class ServerWorld private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun broadcast(packet: IPacket) {
|
override fun broadcast(packet: IPacket) {
|
||||||
internalPlayers.forEach {
|
players.forEach {
|
||||||
it.send(packet)
|
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.math.*
|
||||||
import ru.dbotthepony.kstarbound.network.IPacket
|
import ru.dbotthepony.kstarbound.network.IPacket
|
||||||
import ru.dbotthepony.kstarbound.network.packets.clientbound.SetPlayerStartPacket
|
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.ExceptionLogger
|
||||||
import ru.dbotthepony.kstarbound.util.ParallelPerform
|
import ru.dbotthepony.kstarbound.util.ParallelPerform
|
||||||
import ru.dbotthepony.kstarbound.world.api.ICellAccess
|
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.getBlockPlatforms
|
||||||
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
|
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.util.concurrent.ForkJoinPool
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import java.util.function.Predicate
|
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()
|
Starbound.EXECUTOR.submit(ParallelPerform(dynamicEntities.spliterator(), { if (!it.isRemote) it.movement.move() })).join()
|
||||||
mailbox.executeQueuedTasks()
|
mailbox.executeQueuedTasks()
|
||||||
|
|
||||||
entities.values.forEach { it.think() }
|
entities.values.forEach { it.tick() }
|
||||||
mailbox.executeQueuedTasks()
|
mailbox.executeQueuedTasks()
|
||||||
|
|
||||||
for (chunk in chunkMap) {
|
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.packets.EntityDestroyPacket
|
||||||
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
|
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
|
||||||
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
|
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.Chunk
|
||||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||||
import ru.dbotthepony.kstarbound.world.LightCalculator
|
import ru.dbotthepony.kstarbound.world.LightCalculator
|
||||||
import ru.dbotthepony.kstarbound.world.World
|
import ru.dbotthepony.kstarbound.world.World
|
||||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
@ -148,25 +145,25 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
|
|||||||
|
|
||||||
var isRemote: Boolean = false
|
var isRemote: Boolean = false
|
||||||
|
|
||||||
fun think() {
|
fun tick() {
|
||||||
thinkShared()
|
tickShared()
|
||||||
|
|
||||||
if (isRemote) {
|
if (isRemote) {
|
||||||
thinkRemote()
|
tickRemote()
|
||||||
} else {
|
} else {
|
||||||
thinkLocal()
|
tickLocal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun thinkShared() {
|
protected open fun tickShared() {
|
||||||
mailbox.executeQueuedTasks()
|
mailbox.executeQueuedTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun thinkRemote() {
|
protected open fun tickRemote() {
|
||||||
networkGroup.upstream.tickInterpolation(Starbound.TIMESTEP)
|
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.JsonObject
|
||||||
import com.google.gson.TypeAdapter
|
import com.google.gson.TypeAdapter
|
||||||
import com.google.gson.reflect.TypeToken
|
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.math.RGBAColor
|
||||||
import ru.dbotthepony.kommons.vector.Vector2i
|
import ru.dbotthepony.kommons.vector.Vector2i
|
||||||
import ru.dbotthepony.kstarbound.Registries
|
import ru.dbotthepony.kstarbound.Registries
|
||||||
@ -139,12 +137,12 @@ open class WorldObject(
|
|||||||
super.invalidate()
|
super.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun thinkShared() {
|
override fun tickShared() {
|
||||||
super.thinkShared()
|
super.tickShared()
|
||||||
flickerPeriod?.update(Starbound.TIMESTEP, world.random)
|
flickerPeriod?.update(Starbound.TIMESTEP, world.random)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun thinkRemote() {
|
override fun tickRemote() {
|
||||||
val orientation = orientation
|
val orientation = orientation
|
||||||
|
|
||||||
if (orientation != null) {
|
if (orientation != null) {
|
||||||
|
Loading…
Reference in New Issue
Block a user