Archived
1
0
Fork 0

Refactor and improve movement-related packets, fix keep alive

This commit is contained in:
Moritz Ruth 2021-02-25 11:16:37 +01:00
parent 3767d66065
commit dc156fcf34
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
31 changed files with 183 additions and 123 deletions

View file

@ -1,5 +1,16 @@
# Uranos
Uranos is a game server implementing the Minecraft protocol. That means you can use the official Minecraft client to
join Uranos servers.
Its goal is to be a modern alternative to Bukkit, SpigotMC and Paper. It is primarily intended to make creating custom
games inside of Minecraft easier than it is possible with the existing alternatives, but it can also be used for
something like a lobby/hub server.
The most important thing for Uranos is
[developer experience (DX)](https://medium.com/swlh/what-is-dx-developer-experience-401a0e44a9d9). After that comes
performance.
## Milestones
- Players can see entities
@ -19,6 +30,7 @@
- Entity AI framework
- Scoreboards + Teams
- Crafting
- Rate limiting packets
## Development

View file

@ -13,6 +13,7 @@ import space.uranos.entity.CowEntity
import space.uranos.net.ServerListInfo
import space.uranos.net.event.ServerListInfoRequestEvent
import space.uranos.net.event.SessionAfterLoginEvent
import space.uranos.net.packet.play.EntityHeadYawPacket
import space.uranos.player.GameMode
import space.uranos.plugin.Plugin
import space.uranos.testplugin.anvil.AnvilWorld
@ -22,8 +23,10 @@ import space.uranos.util.secondsToTicks
import space.uranos.world.*
import space.uranos.world.block.GreenWoolBlock
import space.uranos.world.block.RedWoolBlock
import kotlin.random.Random
import kotlin.random.nextUBytes
class TestPlugin: Plugin("Test", "1.0.0") {
class TestPlugin : Plugin("Test", "1.0.0") {
override suspend fun onEnable() {
val dimension = Dimension(
"test:test",
@ -83,9 +86,18 @@ class TestPlugin: Plugin("Test", "1.0.0") {
}
var x = 1.0
Uranos.scheduler.executeRepeating(1) {
Uranos.scheduler.executeRepeating(20) {
x += 0.2
runInServerThread { entity.position = Position(x + 0.5, x, 0.5, 0f, 0f) }
runInServerThread {
Uranos.players.forEach {
it.session.send(
EntityHeadYawPacket(
entity.numericID,
Random.nextUBytes(1)[0]
)
)
}
}
if (x >= 10.0) x = 0.0
}
}

View file

@ -7,16 +7,15 @@ package space.uranos
import space.uranos.world.VoxelLocation
import space.uranos.world.World
import java.lang.IllegalArgumentException
import kotlin.math.roundToInt
/**
* A combination of x, y and z coordinates and an orientation (yaw and pitch).
*
* @param yaw The yaw rotation in degrees. Must be in `[0; 360[`.
* @param pitch The pitch rotation as a value between -90 (up) and 90 (down).
* @param headPitch The pitch rotation of the head as a value between -90 (up) and 90 (down).
*/
data class Position(val x: Double, val y: Double, val z: Double, val yaw: Float, val pitch: Float) {
data class Position(val x: Double, val y: Double, val z: Double, val yaw: Float, val headPitch: Float) {
init {
if (yaw >= 360) throw IllegalArgumentException("yaw must be lower than 360")
if (yaw < 0) throw IllegalArgumentException("yaw must not be negative")
@ -38,8 +37,8 @@ data class Position(val x: Double, val y: Double, val z: Double, val yaw: Float,
fun toVector() = Vector(x, y, z)
infix fun inside(world: World): Pair<World, Position> = world to this
val yawIn256Steps get() = ((yaw / 360) * 256).toInt().toUByte()
val pitchIn256Steps get() = ((yaw / 360) * 256).toInt().toUByte()
val yawIn256Steps: UByte get() = ((yaw / 360) * 256).toInt().toUByte()
val headPitchIn256Steps: UByte get() = ((yaw / 360) * 256).toInt().toUByte()
operator fun get(part: CoordinatePart): Double = when (part) {
CoordinatePart.X -> x

View file

@ -18,7 +18,7 @@ import space.uranos.player.GameMode
import space.uranos.player.Player
import space.uranos.world.World
import java.net.InetAddress
import java.util.*
import java.util.UUID
abstract class Session {
val events by lazy { EventBusWrapper<Session>(this) }
@ -114,14 +114,14 @@ abstract class Session {
)
/**
* Sends a packet.
* Sends a packet instantly.
*/
abstract suspend fun send(packet: OutgoingPacket)
abstract suspend fun sendNow(packet: OutgoingPacket)
/**
* Sends a packet the next tick.
* Adds packet to the queue.
*/
abstract fun sendNextTick(packet: OutgoingPacket)
abstract fun send(packet: OutgoingPacket)
/**
* Sends a plugin message packet.

View file

@ -7,14 +7,14 @@ package space.uranos.util
import kotlin.reflect.KProperty
class MemoizedDelegate<T>(private val dependingGetter: () -> Any?, private val initializer: () -> T) {
class MemoizedDelegate<T>(private val key: () -> Any?, private val initializer: () -> T) {
private object UNINITIALIZED
private var lastDependingValue: Any? = null
private var value: Any? = UNINITIALIZED
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
val currentDependingValue = dependingGetter()
val currentDependingValue = key()
if (value == UNINITIALIZED || (lastDependingValue != currentDependingValue)) {
value = initializer()

View file

@ -17,7 +17,7 @@ import space.uranos.Vector
*/
data class VoxelLocation(val x: Int, val y: UByte, val z: Int) {
/**
* Converts this VoxelLocation to a
* Converts this VoxelLocation to a Location.
*/
fun asLocation(): Location = Location(x.toDouble(), y.toDouble(), z.toDouble())

View file

@ -9,11 +9,11 @@ import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object EntityOrientationPacketCodec :
OutgoingPacketCodec<EntityOrientationPacket>(0x29, EntityOrientationPacket::class) {
override fun EntityOrientationPacket.encode(dst: ByteBuf) {
object EntityHeadPitchPacketCodec :
OutgoingPacketCodec<EntityHeadPitchPacket>(0x29, EntityHeadPitchPacket::class) {
override fun EntityHeadPitchPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeByte(yaw.toInt())
dst.writeByte(0) // Should be yaw, but is actually ignored. Use EntityHeadYawPacket instead.
dst.writeByte(pitch.toInt())
dst.writeBoolean(onGround)
}

View file

@ -0,0 +1,17 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object EntityHeadYawPacketCodec : OutgoingPacketCodec<EntityHeadYawPacket>(0x3A, EntityHeadYawPacket::class) {
override fun EntityHeadYawPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeByte(yaw.toInt())
}
}

View file

@ -17,7 +17,7 @@ object EntityRelativeMoveWithOrientationPacketCodec :
dst.writeShort(deltaY.toInt())
dst.writeShort(deltaZ.toInt())
dst.writeByte(yaw.toInt())
dst.writeByte(pitch.toInt())
dst.writeByte(headPitch.toInt())
dst.writeBoolean(onGround)
}
}

View file

@ -13,11 +13,11 @@ object EntityTeleportPacketCodec :
OutgoingPacketCodec<EntityTeleportPacket>(0x56, EntityTeleportPacket::class) {
override fun EntityTeleportPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID)
dst.writeDouble(position.x)
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt())
dst.writeByte(position.pitchIn256Steps.toInt())
dst.writeDouble(x)
dst.writeDouble(y)
dst.writeDouble(z)
dst.writeByte(yaw.toInt())
dst.writeByte(headPitch.toInt())
dst.writeBoolean(onGround)
}
}

View file

@ -17,7 +17,7 @@ object OutgoingPlayerPositionPacketCodec :
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeFloat(position.yaw)
dst.writeFloat(position.pitch)
dst.writeFloat(position.headPitch)
dst.writeByte(bitmask(relativeX, relativeY, relativeZ, relativeYaw, relativePitch))
dst.writeVarInt(0) // Teleport ID, I am not sure why this is needed
}

View file

@ -18,7 +18,8 @@ object PlayProtocol : Protocol(
DeclareRecipesPacketCodec,
DestroyEntitiesPacketCodec,
DisconnectPacketCodec,
EntityOrientationPacketCodec,
EntityHeadPitchPacketCodec,
EntityHeadYawPacketCodec,
EntityRelativeMovePacketCodec,
EntityRelativeMoveWithOrientationPacketCodec,
EntityTeleportPacketCodec,
@ -40,5 +41,5 @@ object PlayProtocol : Protocol(
SpawnObjectEntityPacketCodec,
SpawnPaintingPacketCodec,
TagsPacketCodec,
UpdateViewPositionPacketCodec
ViewPositionPacketCodec
)

View file

@ -20,7 +20,7 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacke
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt())
dst.writeByte(position.pitchIn256Steps.toInt())
dst.writeByte(position.headPitchIn256Steps.toInt())
dst.writeByte(0) // Head pitch; I do not know what this does
dst.writeShort((velocity.x * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.y * 8000).toInt().toShort().toInt())

View file

@ -20,7 +20,7 @@ object SpawnObjectEntityPacketCodec : OutgoingPacketCodec<SpawnObjectEntityPacke
dst.writeDouble(position.y)
dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt())
dst.writeByte(position.pitchIn256Steps.toInt())
dst.writeByte(position.headPitchIn256Steps.toInt())
dst.writeInt(data)
dst.writeShort((velocity.x * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.y * 8000).toInt().toShort().toInt())

View file

@ -9,9 +9,9 @@ import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
object UpdateViewPositionPacketCodec :
OutgoingPacketCodec<UpdateViewPositionPacket>(0x40, UpdateViewPositionPacket::class) {
override fun UpdateViewPositionPacket.encode(dst: ByteBuf) {
object ViewPositionPacketCodec :
OutgoingPacketCodec<ViewPositionPacket>(0x40, ViewPositionPacket::class) {
override fun ViewPositionPacket.encode(dst: ByteBuf) {
dst.writeVarInt(chunkKey.x)
dst.writeVarInt(chunkKey.z)
}

View file

@ -7,9 +7,8 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
data class EntityOrientationPacket(
data class EntityHeadPitchPacket(
val entityID: Int,
val yaw: UByte,
val pitch: UByte,
val onGround: Boolean
) : OutgoingPacket()

View file

@ -0,0 +1,13 @@
/*
* Copyright 2020-2021 Moritz Ruth and Uranos contributors
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file
*/
package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
data class EntityHeadYawPacket(
val entityID: Int,
val yaw: UByte
) : OutgoingPacket()

View file

@ -19,6 +19,6 @@ data class EntityRelativeMoveWithOrientationPacket(
/**
* Absolute value.
*/
val pitch: UByte,
val headPitch: UByte,
val onGround: Boolean
) : OutgoingPacket()

View file

@ -10,6 +10,20 @@ import space.uranos.net.packet.OutgoingPacket
data class EntityTeleportPacket(
val entityID: Int,
val position: Position,
val x: Double,
val y: Double,
val z: Double,
val yaw: UByte,
val headPitch: UByte,
val onGround: Boolean
) : OutgoingPacket()
) : OutgoingPacket() {
constructor(entityID: Int, position: Position, onGround: Boolean) : this(
entityID,
position.x,
position.y,
position.z,
position.yawIn256Steps,
position.headPitchIn256Steps,
onGround
)
}

View file

@ -12,4 +12,4 @@ import space.uranos.world.Chunk
* The reason why this packet exists and what it does is not clear at the moment of writing.
* It it sent every time the player crosses a chunk or chunk section border.
*/
data class UpdateViewPositionPacket(val chunkKey: Chunk.Key) : OutgoingPacket()
data class ViewPositionPacket(val chunkKey: Chunk.Key) : OutgoingPacket()

View file

@ -8,21 +8,20 @@ package space.uranos.util
import space.uranos.Position
import space.uranos.abs
import space.uranos.net.packet.OutgoingPacket
import space.uranos.net.packet.play.EntityOrientationPacket
import space.uranos.net.packet.play.EntityHeadPitchPacket
import space.uranos.net.packet.play.EntityRelativeMovePacket
import space.uranos.net.packet.play.EntityRelativeMoveWithOrientationPacket
import space.uranos.net.packet.play.EntityTeleportPacket
fun createEntityMovementPacket(entityID: Int, oldPosition: Position, newPosition: Position): OutgoingPacket? {
val delta = abs(newPosition.toVector() - oldPosition.toVector())
val orientationChanged = oldPosition.yaw != newPosition.yaw || oldPosition.pitch != newPosition.pitch
val orientationChanged = oldPosition.yaw != newPosition.yaw || oldPosition.headPitch != newPosition.headPitch
val onGround = true // TODO: Find out what onGround does
return if (delta.x + delta.y + delta.z == 0.0) {
if (orientationChanged) EntityOrientationPacket(
if (orientationChanged) EntityHeadPitchPacket(
entityID,
newPosition.yawIn256Steps,
newPosition.pitchIn256Steps,
newPosition.headPitchIn256Steps,
onGround
) else null
} else if (delta.x > 8 || delta.y > 8 || delta.z > 8) {
@ -34,7 +33,7 @@ fun createEntityMovementPacket(entityID: Int, oldPosition: Position, newPosition
getDeltaShort(delta.y),
getDeltaShort(delta.z),
newPosition.yawIn256Steps,
newPosition.pitchIn256Steps,
newPosition.headPitchIn256Steps,
onGround
) else EntityRelativeMovePacket(
entityID,

View file

@ -10,9 +10,8 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import space.uranos.logging.Logger
import space.uranos.server.Server
import java.util.*
import java.util.Collections
import java.util.concurrent.*
import kotlin.collections.LinkedHashSet
import kotlin.coroutines.resume
/**
@ -35,6 +34,8 @@ class UranosScheduler : Scheduler {
@Volatile
var cancelled: Boolean = false
val creationStackTrace = Thread.currentThread().stackTrace
override fun cancel() {
tasks.remove(this)
cancelled = true

View file

@ -133,22 +133,28 @@ class UranosServer internal constructor() : Server() {
}
private fun startTicking() {
var index = 0
scheduler.executeRepeating(1, 0) {
runInServerThread {
players.forEach { it.container.tick() }
internalEntities.forEach { it.tick() }
sessions.forEach { it.packetsAdapter.tick() }
sessions.forEach { it.tick(index) }
}
index++
}
}
private fun startPingSync() {
// Not in UranosSession.tick because this simplifies reusing the packet and the ping is not a critical information
scheduler.executeRepeating(msToTicks(config.pingUpdateInterval.toMillis()), 0) {
val packet = PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency(players.map { it.uuid to it.session.ping }.toMap())
)
players.forEach { it.session.send(packet) }
players.forEach {
it.session.send(packet)
}
}
}
@ -156,8 +162,8 @@ class UranosServer internal constructor() : Server() {
eventBus.on<ViewingChangedEvent>(EventHandlerPosition.LAST) { event ->
if (event.target == event.player.entity) return@on
if (event.viewing) event.player.session.sendNextTick(event.target.createSpawnPacket())
else event.player.session.sendNextTick(DestroyEntitiesPacket(arrayOf(event.target.numericID)))
if (event.viewing) event.player.session.send(event.target.createSpawnPacket())
else event.player.session.send(DestroyEntitiesPacket(arrayOf(event.target.numericID)))
}
}

View file

@ -69,7 +69,7 @@ sealed class UranosEntity(server: UranosServer) : Entity {
}
}
suspend fun tick() {
open suspend fun tick() {
container.tick()
}
}
@ -81,7 +81,7 @@ abstract class UranosLivingEntity(server: UranosServer) : UranosEntity(server),
override var position: Position by container.ifChanged(Position.ZERO) { value ->
if (viewers.isNotEmpty()) createEntityMovementPacket(numericID, lastSentPosition, value)?.let {
for (viewer in viewers) {
viewer.session.sendNextTick(it)
viewer.session.send(it)
}
}
@ -98,7 +98,7 @@ abstract class UranosObjectEntity(server: UranosServer) : UranosEntity(server),
override var position: Position by container.ifChanged(Position.ZERO) { value ->
createEntityMovementPacket(numericID, lastSentPosition, value)?.let {
for (viewer in viewers) {
viewer.session.sendNextTick(it)
viewer.session.send(it)
}
}
}

View file

@ -22,7 +22,7 @@ import space.uranos.util.AuthenticationHelper
import space.uranos.util.EncryptionUtils
import space.uranos.world.Chunk
import java.security.MessageDigest
import java.util.*
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
@ -35,10 +35,10 @@ class LoginAndJoinProcedure(val session: UranosSession) {
if (session.server.config.authenticateAndEncrypt) {
val verifyToken = EncryptionUtils.generateVerifyToken()
session.state = Session.State.WaitingForEncryptionVerification(packet.username, verifyToken)
session.send(EncryptionRequestPacket(session.server.x509EncodedPublicKey, verifyToken))
session.sendNow(EncryptionRequestPacket(session.server.x509EncodedPublicKey, verifyToken))
} else {
val uuid = UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray())
session.send(LoginSuccessPacket(uuid, packet.username))
session.sendNow(LoginSuccessPacket(uuid, packet.username))
session.state = Session.State.LoginSucceeded(packet.username, uuid)
afterLogin()
}
@ -71,10 +71,10 @@ class LoginAndJoinProcedure(val session: UranosSession) {
val result = AuthenticationHelper.authenticate(hashString, state.username)
session.send(SetCompressionPacket(session.server.config.packetCompressionThreshold))
session.sendNow(SetCompressionPacket(session.server.config.packetCompressionThreshold))
session.enableCompressionCodec()
session.send(LoginSuccessPacket(result.uuid, result.username))
session.sendNow(LoginSuccessPacket(result.uuid, result.username))
session.state = Session.State.LoginSucceeded(result.username, result.uuid)
afterLogin()
}
@ -93,7 +93,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
session.disconnect(internalReason = "No spawn location set")
}
else -> {
session.send(
session.sendNow(
JoinGamePacket(
0,
event.gameMode,
@ -111,9 +111,9 @@ class LoginAndJoinProcedure(val session: UranosSession) {
)
// As this is only visual, there is no way of changing it aside from intercepting the packet.
session.send(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
session.sendNow(ServerDifficultyPacket(DifficultySettings(Difficulty.NORMAL, false)))
session.send(
session.sendNow(
PlayerAbilitiesPacket(
event.invulnerable,
event.flying,
@ -174,17 +174,17 @@ class LoginAndJoinProcedure(val session: UranosSession) {
session.state = Session.State.Joining(player)
session.send(SelectedHotbarSlotPacket(state.selectedHotbarSlot))
session.sendNow(SelectedHotbarSlotPacket(state.selectedHotbarSlot))
session.send(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
session.send(tagsPacket)
session.sendNow(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
session.sendNow(tagsPacket)
// session.send(DeclareCommandsPacket(session.server.commandRegistry.items.values))
// UnlockRecipes
session.send(OutgoingPlayerPositionPacket(state.position))
session.sendNow(OutgoingPlayerPositionPacket(state.position))
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map {
session.sendNow(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map {
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
it.name,
it.gameMode,
@ -194,7 +194,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
)
}.toMap())))
session.send(
session.sendNow(
PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency(
session.server.players.map { it.uuid to it.session.ping }.toMap()
@ -202,14 +202,14 @@ class LoginAndJoinProcedure(val session: UranosSession) {
)
)
session.send(UpdateViewPositionPacket(Chunk.Key.from(player.entity.position.toVoxelLocation())))
session.sendNow(ViewPositionPacket(Chunk.Key.from(player.entity.position.toVoxelLocation())))
session.scheduleKeepAlivePacket(true)
session.sendKeepAlivePacket()
player.spawnInitially(state.world)
session.state = Session.State.Playing(player)
// WorldBorder
session.sendNextTick(CompassTargetPacket(player.compassTarget))
session.send(CompassTargetPacket(player.compassTarget))
}
}

View file

@ -37,7 +37,7 @@ class PacketsAdapter(val session: UranosSession) {
}
private suspend fun handlePacket(packet: IncomingPacket) {
if (session.server.config.logging.shouldLog(packet)) session.logger.trace { "Packet received: $packet" }
if (session.server.config.logging.shouldLog(packet)) session.logger.info { "Packet received: $packet" }
session.server.eventBus.emit(PacketReceivedEvent(session, packet)).ifNotCancelled {
try {
@ -70,7 +70,7 @@ class PacketsAdapter(val session: UranosSession) {
}
suspend fun send(packet: OutgoingPacket) {
if (session.server.config.logging.shouldLog(packet)) session.logger.trace { "Sending packet: $packet" }
if (session.server.config.logging.shouldLog(packet)) session.logger.info { "Sending packet: $packet" }
session.server.eventBus.emit(PacketSendEvent(session, packet)).ifNotCancelled {
try {
@ -118,6 +118,7 @@ class PacketsAdapter(val session: UranosSession) {
session.logger warn "$message. This will cause the client to disconnect in production mode."
else session.failAndDisconnectBecauseOfClient(message)
ReferenceCountUtil.release(data)
return
}

View file

@ -23,10 +23,10 @@ import space.uranos.net.packet.play.PlayProtocol
import space.uranos.net.packet.play.PlayerInfoPacket
import space.uranos.net.packet.status.StatusProtocol
import space.uranos.server.event.SessionInitializedEvent
import space.uranos.util.msToTicks
import java.net.InetAddress
import java.net.InetSocketAddress
import javax.crypto.SecretKey
import kotlin.properties.Delegates
class UranosSession(val channel: io.netty.channel.Channel, val server: UranosServer) : Session() {
override val address: InetAddress = (channel.remoteAddress() as InetSocketAddress).address
@ -38,9 +38,13 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
override var ping: Int = -1
set(value) {
if (field == -1) {
val packet =
PlayerInfoPacket(PlayerInfoPacket.Action.UpdateLatency(mapOf((state as State.WithPlayer).player.uuid to value)))
val player = earlyPlayer
if (player != null && field == -1) {
val packet = PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency(mapOf(player.uuid to value))
)
scope.launch { server.players.forEach { it.session.send(packet) } }
}
@ -71,12 +75,12 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
val packetsAdapter = PacketsAdapter(this)
override suspend fun send(packet: OutgoingPacket) = packetsAdapter.send(packet)
override fun sendNextTick(packet: OutgoingPacket) = packetsAdapter.sendNextTick(packet)
override suspend fun sendNow(packet: OutgoingPacket) = packetsAdapter.send(packet)
override fun send(packet: OutgoingPacket) = packetsAdapter.sendNextTick(packet)
override suspend fun sendPluginMessage(channel: String, data: ByteBuf) {
if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol")
send(OutgoingPluginMessagePacket(channel, data))
sendNow(OutgoingPluginMessagePacket(channel, data))
data.release()
}
@ -99,8 +103,8 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
} else reason
when (currentProtocol) {
LoginProtocol -> send(space.uranos.net.packet.login.DisconnectPacket(finalReason))
PlayProtocol -> send(space.uranos.net.packet.play.DisconnectPacket(finalReason))
LoginProtocol -> sendNow(space.uranos.net.packet.login.DisconnectPacket(finalReason))
PlayProtocol -> sendNow(space.uranos.net.packet.play.DisconnectPacket(finalReason))
}
} else logger trace "Disconnected"
@ -110,30 +114,6 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
val joinProcedure = LoginAndJoinProcedure(this)
var lastKeepAlivePacketTimestamp by Delegates.notNull<Long>()
lateinit var keepAliveDisconnectJob: Job
fun scheduleKeepAlivePacket(isFirst: Boolean = false) {
scope.launch { // TODO: Fix random disconnects (maybe some response packets are skipped?)
if (!isFirst) {
val timeSinceLastPacket = (System.currentTimeMillis() - lastKeepAlivePacketTimestamp).toInt()
delay(KEEP_ALIVE_PACKET_INTERVAL.toLong() - timeSinceLastPacket)
}
lastKeepAlivePacketTimestamp = System.currentTimeMillis()
send(OutgoingKeepAlivePacket(lastKeepAlivePacketTimestamp))
keepAliveDisconnectJob = launch {
delay(server.config.timeout.toMillis())
disconnect(
TextComponent of "Timed out",
"The client did not respond to the KeepAlive packet for " +
"${server.config.timeout.toMillis()}ms"
)
}
}
}
fun onConnect() = scope.launch {
logger trace "Connected"
packetsAdapter.launchPacketDataChannelConsumer()
@ -147,7 +127,7 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
scope.launch {
if (state == State.Disconnected) return@launch
if (!expected && currentProtocol != HandshakingProtocol && currentProtocol != StatusProtocol)
logger trace "The client disconnected unexpectedly" // TODO: This is sometimes logged multiple times
logger trace "The client disconnected unexpectedly"
packetsAdapter.stopProcessingIncomingPackets()
coroutineContext.cancel(DisconnectedCancellationException())
@ -192,7 +172,15 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
.addAfter("framing", "compression", CompressionCodec(server.config.packetCompressionThreshold))
}
fun sendKeepAlivePacket() = send(OutgoingKeepAlivePacket(System.currentTimeMillis()))
suspend fun tick(index: Int) {
if (earlyPlayer != null && index % KEEP_ALIVE_PACKET_INTERVAL == 0L) sendKeepAlivePacket()
packetsAdapter.tick()
}
companion object {
const val KEEP_ALIVE_PACKET_INTERVAL = 1000
val KEEP_ALIVE_PACKET_INTERVAL = msToTicks(1000)
}
}

View file

@ -5,17 +5,15 @@
package space.uranos.net.packet.play
import space.uranos.chat.TextComponent
import space.uranos.net.PacketReceivedEventHandler
import space.uranos.net.UranosSession
object IncomingKeepAlivePacketHandler : PacketReceivedEventHandler<IncomingKeepAlivePacket>() {
override suspend fun handle(session: UranosSession, packet: IncomingKeepAlivePacket) {
if (session.lastKeepAlivePacketTimestamp == packet.id) {
session.ping = (System.currentTimeMillis() - session.lastKeepAlivePacketTimestamp).toInt()
session.keepAliveDisconnectJob.cancel()
session.scheduleKeepAlivePacket()
} else {
session.disconnect(internalReason = "The ID of the last IncomingKeepAlive packet does not match the expected one.")
}
val ping = (System.currentTimeMillis() - packet.id).toInt()
if (ping >= session.server.config.timeout.toMillis()) session.disconnect(TextComponent of "Timed out")
else session.ping = ping
}
}

View file

@ -10,7 +10,7 @@ import space.uranos.net.UranosSession
object PlayerOrientationPacketHandler : PacketReceivedEventHandler<PlayerOrientationPacket>() {
override suspend fun handle(session: UranosSession, packet: PlayerOrientationPacket) {
session.earlyPlayer?.entity?.let { it.position = it.position.copy(yaw = packet.yaw, pitch = packet.pitch) }
session.earlyPlayer?.entity?.let { it.position = it.position.copy(yaw = packet.yaw, headPitch = packet.pitch) }
?: error("Player not yet initialized")
}
}

View file

@ -19,11 +19,11 @@ object StatusProtocolHandler : ProtocolPacketReceivedEventHandler(mapOf(
response == null ->
session.disconnect(internalReason = "The response property of the ServerListInfoRequestEvent is null.")
else -> session.send(ResponsePacket(response))
else -> session.sendNow(ResponsePacket(response))
}
},
PingPacket::class to PacketReceivedEventHandler.of<PingPacket> { session, packet ->
session.send(PongPacket(packet.payload))
session.sendNow(PongPacket(packet.payload))
session.disconnect()
}
))

View file

@ -44,12 +44,12 @@ class UranosPlayer(
override var selectedHotbarSlot by container.ifChanged(
selectedHotbarSlot,
{ clampArgument("selectedHotbarSlot", 0..8, it) }) {
session.send(SelectedHotbarSlotPacket(it))
session.sendNow(SelectedHotbarSlotPacket(it))
}
override var playerListName by container.ifChanged<TextComponent?>(TextComponent of name) { value ->
session.server.players.forEach {
it.session.sendNextTick(PlayerInfoPacket(PlayerInfoPacket.Action.UpdateDisplayName(mapOf(uuid to value))))
it.session.send(PlayerInfoPacket(PlayerInfoPacket.Action.UpdateDisplayName(mapOf(uuid to value))))
}
}
@ -90,7 +90,7 @@ class UranosPlayer(
private suspend fun sendChunksAndLight() {
val chunks = currentlyViewedChunks.sortedBy { abs(it.key.x) + abs(it.key.z) }
chunks.forEach { session.send(ChunkLightDataPacket(it.key, it.getLightData(this))) }
chunks.forEach { session.send(ChunkDataPacket(it.key, it.getData(this))) }
chunks.forEach { session.sendNow(ChunkLightDataPacket(it.key, it.getLightData(this))) }
chunks.forEach { session.sendNow(ChunkDataPacket(it.key, it.getData(this))) }
}
}