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
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 ## Milestones
- Players can see entities - Players can see entities
@ -19,6 +30,7 @@
- Entity AI framework - Entity AI framework
- Scoreboards + Teams - Scoreboards + Teams
- Crafting - Crafting
- Rate limiting packets
## Development ## Development

View file

@ -13,6 +13,7 @@ import space.uranos.entity.CowEntity
import space.uranos.net.ServerListInfo import space.uranos.net.ServerListInfo
import space.uranos.net.event.ServerListInfoRequestEvent import space.uranos.net.event.ServerListInfoRequestEvent
import space.uranos.net.event.SessionAfterLoginEvent import space.uranos.net.event.SessionAfterLoginEvent
import space.uranos.net.packet.play.EntityHeadYawPacket
import space.uranos.player.GameMode import space.uranos.player.GameMode
import space.uranos.plugin.Plugin import space.uranos.plugin.Plugin
import space.uranos.testplugin.anvil.AnvilWorld import space.uranos.testplugin.anvil.AnvilWorld
@ -22,8 +23,10 @@ import space.uranos.util.secondsToTicks
import space.uranos.world.* import space.uranos.world.*
import space.uranos.world.block.GreenWoolBlock import space.uranos.world.block.GreenWoolBlock
import space.uranos.world.block.RedWoolBlock 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() { override suspend fun onEnable() {
val dimension = Dimension( val dimension = Dimension(
"test:test", "test:test",
@ -83,9 +86,18 @@ class TestPlugin: Plugin("Test", "1.0.0") {
} }
var x = 1.0 var x = 1.0
Uranos.scheduler.executeRepeating(1) { Uranos.scheduler.executeRepeating(20) {
x += 0.2 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 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.VoxelLocation
import space.uranos.world.World import space.uranos.world.World
import java.lang.IllegalArgumentException
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
* A combination of x, y and z coordinates and an orientation (yaw and pitch). * 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 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 { init {
if (yaw >= 360) throw IllegalArgumentException("yaw must be lower than 360") if (yaw >= 360) throw IllegalArgumentException("yaw must be lower than 360")
if (yaw < 0) throw IllegalArgumentException("yaw must not be negative") 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) fun toVector() = Vector(x, y, z)
infix fun inside(world: World): Pair<World, Position> = world to this infix fun inside(world: World): Pair<World, Position> = world to this
val yawIn256Steps get() = ((yaw / 360) * 256).toInt().toUByte() val yawIn256Steps: UByte get() = ((yaw / 360) * 256).toInt().toUByte()
val pitchIn256Steps get() = ((yaw / 360) * 256).toInt().toUByte() val headPitchIn256Steps: UByte get() = ((yaw / 360) * 256).toInt().toUByte()
operator fun get(part: CoordinatePart): Double = when (part) { operator fun get(part: CoordinatePart): Double = when (part) {
CoordinatePart.X -> x CoordinatePart.X -> x

View file

@ -18,7 +18,7 @@ import space.uranos.player.GameMode
import space.uranos.player.Player import space.uranos.player.Player
import space.uranos.world.World import space.uranos.world.World
import java.net.InetAddress import java.net.InetAddress
import java.util.* import java.util.UUID
abstract class Session { abstract class Session {
val events by lazy { EventBusWrapper<Session>(this) } 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. * Sends a plugin message packet.

View file

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

View file

@ -17,7 +17,7 @@ import space.uranos.Vector
*/ */
data class VoxelLocation(val x: Int, val y: UByte, val z: Int) { 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()) 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.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec import space.uranos.net.packet.OutgoingPacketCodec
object EntityOrientationPacketCodec : object EntityHeadPitchPacketCodec :
OutgoingPacketCodec<EntityOrientationPacket>(0x29, EntityOrientationPacket::class) { OutgoingPacketCodec<EntityHeadPitchPacket>(0x29, EntityHeadPitchPacket::class) {
override fun EntityOrientationPacket.encode(dst: ByteBuf) { override fun EntityHeadPitchPacket.encode(dst: ByteBuf) {
dst.writeVarInt(entityID) 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.writeByte(pitch.toInt())
dst.writeBoolean(onGround) 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(deltaY.toInt())
dst.writeShort(deltaZ.toInt()) dst.writeShort(deltaZ.toInt())
dst.writeByte(yaw.toInt()) dst.writeByte(yaw.toInt())
dst.writeByte(pitch.toInt()) dst.writeByte(headPitch.toInt())
dst.writeBoolean(onGround) dst.writeBoolean(onGround)
} }
} }

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec<SpawnLivingEntityPacke
dst.writeDouble(position.y) dst.writeDouble(position.y)
dst.writeDouble(position.z) dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt()) 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.writeByte(0) // Head pitch; I do not know what this does
dst.writeShort((velocity.x * 8000).toInt().toShort().toInt()) dst.writeShort((velocity.x * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.y * 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.y)
dst.writeDouble(position.z) dst.writeDouble(position.z)
dst.writeByte(position.yawIn256Steps.toInt()) dst.writeByte(position.yawIn256Steps.toInt())
dst.writeByte(position.pitchIn256Steps.toInt()) dst.writeByte(position.headPitchIn256Steps.toInt())
dst.writeInt(data) dst.writeInt(data)
dst.writeShort((velocity.x * 8000).toInt().toShort().toInt()) dst.writeShort((velocity.x * 8000).toInt().toShort().toInt())
dst.writeShort((velocity.y * 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.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec import space.uranos.net.packet.OutgoingPacketCodec
object UpdateViewPositionPacketCodec : object ViewPositionPacketCodec :
OutgoingPacketCodec<UpdateViewPositionPacket>(0x40, UpdateViewPositionPacket::class) { OutgoingPacketCodec<ViewPositionPacket>(0x40, ViewPositionPacket::class) {
override fun UpdateViewPositionPacket.encode(dst: ByteBuf) { override fun ViewPositionPacket.encode(dst: ByteBuf) {
dst.writeVarInt(chunkKey.x) dst.writeVarInt(chunkKey.x)
dst.writeVarInt(chunkKey.z) dst.writeVarInt(chunkKey.z)
} }

View file

@ -7,9 +7,8 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket import space.uranos.net.packet.OutgoingPacket
data class EntityOrientationPacket( data class EntityHeadPitchPacket(
val entityID: Int, val entityID: Int,
val yaw: UByte,
val pitch: UByte, val pitch: UByte,
val onGround: Boolean val onGround: Boolean
) : OutgoingPacket() ) : 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. * Absolute value.
*/ */
val pitch: UByte, val headPitch: UByte,
val onGround: Boolean val onGround: Boolean
) : OutgoingPacket() ) : OutgoingPacket()

View file

@ -10,6 +10,20 @@ import space.uranos.net.packet.OutgoingPacket
data class EntityTeleportPacket( data class EntityTeleportPacket(
val entityID: Int, val entityID: Int,
val position: Position, val x: Double,
val y: Double,
val z: Double,
val yaw: UByte,
val headPitch: UByte,
val onGround: Boolean 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. * 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. * 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.Position
import space.uranos.abs import space.uranos.abs
import space.uranos.net.packet.OutgoingPacket 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.EntityRelativeMovePacket
import space.uranos.net.packet.play.EntityRelativeMoveWithOrientationPacket import space.uranos.net.packet.play.EntityRelativeMoveWithOrientationPacket
import space.uranos.net.packet.play.EntityTeleportPacket import space.uranos.net.packet.play.EntityTeleportPacket
fun createEntityMovementPacket(entityID: Int, oldPosition: Position, newPosition: Position): OutgoingPacket? { fun createEntityMovementPacket(entityID: Int, oldPosition: Position, newPosition: Position): OutgoingPacket? {
val delta = abs(newPosition.toVector() - oldPosition.toVector()) 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 val onGround = true // TODO: Find out what onGround does
return if (delta.x + delta.y + delta.z == 0.0) { return if (delta.x + delta.y + delta.z == 0.0) {
if (orientationChanged) EntityOrientationPacket( if (orientationChanged) EntityHeadPitchPacket(
entityID, entityID,
newPosition.yawIn256Steps, newPosition.headPitchIn256Steps,
newPosition.pitchIn256Steps,
onGround onGround
) else null ) else null
} else if (delta.x > 8 || delta.y > 8 || delta.z > 8) { } 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.y),
getDeltaShort(delta.z), getDeltaShort(delta.z),
newPosition.yawIn256Steps, newPosition.yawIn256Steps,
newPosition.pitchIn256Steps, newPosition.headPitchIn256Steps,
onGround onGround
) else EntityRelativeMovePacket( ) else EntityRelativeMovePacket(
entityID, entityID,

View file

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

View file

@ -133,22 +133,28 @@ class UranosServer internal constructor() : Server() {
} }
private fun startTicking() { private fun startTicking() {
var index = 0
scheduler.executeRepeating(1, 0) { scheduler.executeRepeating(1, 0) {
runInServerThread { runInServerThread {
players.forEach { it.container.tick() } players.forEach { it.container.tick() }
internalEntities.forEach { it.tick() } internalEntities.forEach { it.tick() }
sessions.forEach { it.packetsAdapter.tick() } sessions.forEach { it.tick(index) }
} }
index++
} }
} }
private fun startPingSync() { 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) { scheduler.executeRepeating(msToTicks(config.pingUpdateInterval.toMillis()), 0) {
val packet = PlayerInfoPacket( val packet = PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency(players.map { it.uuid to it.session.ping }.toMap()) 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 -> eventBus.on<ViewingChangedEvent>(EventHandlerPosition.LAST) { event ->
if (event.target == event.player.entity) return@on if (event.target == event.player.entity) return@on
if (event.viewing) event.player.session.sendNextTick(event.target.createSpawnPacket()) if (event.viewing) event.player.session.send(event.target.createSpawnPacket())
else event.player.session.sendNextTick(DestroyEntitiesPacket(arrayOf(event.target.numericID))) 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() container.tick()
} }
} }
@ -81,7 +81,7 @@ abstract class UranosLivingEntity(server: UranosServer) : UranosEntity(server),
override var position: Position by container.ifChanged(Position.ZERO) { value -> override var position: Position by container.ifChanged(Position.ZERO) { value ->
if (viewers.isNotEmpty()) createEntityMovementPacket(numericID, lastSentPosition, value)?.let { if (viewers.isNotEmpty()) createEntityMovementPacket(numericID, lastSentPosition, value)?.let {
for (viewer in viewers) { 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 -> override var position: Position by container.ifChanged(Position.ZERO) { value ->
createEntityMovementPacket(numericID, lastSentPosition, value)?.let { createEntityMovementPacket(numericID, lastSentPosition, value)?.let {
for (viewer in viewers) { 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.util.EncryptionUtils
import space.uranos.world.Chunk import space.uranos.world.Chunk
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.UUID
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -35,10 +35,10 @@ class LoginAndJoinProcedure(val session: UranosSession) {
if (session.server.config.authenticateAndEncrypt) { if (session.server.config.authenticateAndEncrypt) {
val verifyToken = EncryptionUtils.generateVerifyToken() val verifyToken = EncryptionUtils.generateVerifyToken()
session.state = Session.State.WaitingForEncryptionVerification(packet.username, verifyToken) session.state = Session.State.WaitingForEncryptionVerification(packet.username, verifyToken)
session.send(EncryptionRequestPacket(session.server.x509EncodedPublicKey, verifyToken)) session.sendNow(EncryptionRequestPacket(session.server.x509EncodedPublicKey, verifyToken))
} else { } else {
val uuid = UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray()) 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) session.state = Session.State.LoginSucceeded(packet.username, uuid)
afterLogin() afterLogin()
} }
@ -71,10 +71,10 @@ class LoginAndJoinProcedure(val session: UranosSession) {
val result = AuthenticationHelper.authenticate(hashString, state.username) val result = AuthenticationHelper.authenticate(hashString, state.username)
session.send(SetCompressionPacket(session.server.config.packetCompressionThreshold)) session.sendNow(SetCompressionPacket(session.server.config.packetCompressionThreshold))
session.enableCompressionCodec() 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) session.state = Session.State.LoginSucceeded(result.username, result.uuid)
afterLogin() afterLogin()
} }
@ -93,7 +93,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
session.disconnect(internalReason = "No spawn location set") session.disconnect(internalReason = "No spawn location set")
} }
else -> { else -> {
session.send( session.sendNow(
JoinGamePacket( JoinGamePacket(
0, 0,
event.gameMode, 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. // 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( PlayerAbilitiesPacket(
event.invulnerable, event.invulnerable,
event.flying, event.flying,
@ -174,17 +174,17 @@ class LoginAndJoinProcedure(val session: UranosSession) {
session.state = Session.State.Joining(player) 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.sendNow(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
session.send(tagsPacket) session.sendNow(tagsPacket)
// session.send(DeclareCommandsPacket(session.server.commandRegistry.items.values)) // session.send(DeclareCommandsPacket(session.server.commandRegistry.items.values))
// UnlockRecipes // 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.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
it.name, it.name,
it.gameMode, it.gameMode,
@ -194,7 +194,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
) )
}.toMap()))) }.toMap())))
session.send( session.sendNow(
PlayerInfoPacket( PlayerInfoPacket(
PlayerInfoPacket.Action.UpdateLatency( PlayerInfoPacket.Action.UpdateLatency(
session.server.players.map { it.uuid to it.session.ping }.toMap() 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) player.spawnInitially(state.world)
session.state = Session.State.Playing(player) session.state = Session.State.Playing(player)
// WorldBorder // 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) { 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 { session.server.eventBus.emit(PacketReceivedEvent(session, packet)).ifNotCancelled {
try { try {
@ -70,7 +70,7 @@ class PacketsAdapter(val session: UranosSession) {
} }
suspend fun send(packet: OutgoingPacket) { 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 { session.server.eventBus.emit(PacketSendEvent(session, packet)).ifNotCancelled {
try { try {
@ -118,6 +118,7 @@ class PacketsAdapter(val session: UranosSession) {
session.logger warn "$message. This will cause the client to disconnect in production mode." session.logger warn "$message. This will cause the client to disconnect in production mode."
else session.failAndDisconnectBecauseOfClient(message) else session.failAndDisconnectBecauseOfClient(message)
ReferenceCountUtil.release(data)
return 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.play.PlayerInfoPacket
import space.uranos.net.packet.status.StatusProtocol import space.uranos.net.packet.status.StatusProtocol
import space.uranos.server.event.SessionInitializedEvent import space.uranos.server.event.SessionInitializedEvent
import space.uranos.util.msToTicks
import java.net.InetAddress import java.net.InetAddress
import java.net.InetSocketAddress import java.net.InetSocketAddress
import javax.crypto.SecretKey import javax.crypto.SecretKey
import kotlin.properties.Delegates
class UranosSession(val channel: io.netty.channel.Channel, val server: UranosServer) : Session() { class UranosSession(val channel: io.netty.channel.Channel, val server: UranosServer) : Session() {
override val address: InetAddress = (channel.remoteAddress() as InetSocketAddress).address 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 override var ping: Int = -1
set(value) { set(value) {
if (field == -1) { val player = earlyPlayer
val packet =
PlayerInfoPacket(PlayerInfoPacket.Action.UpdateLatency(mapOf((state as State.WithPlayer).player.uuid to value))) 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) } } 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) val packetsAdapter = PacketsAdapter(this)
override suspend fun send(packet: OutgoingPacket) = packetsAdapter.send(packet) override suspend fun sendNow(packet: OutgoingPacket) = packetsAdapter.send(packet)
override fun sendNextTick(packet: OutgoingPacket) = packetsAdapter.sendNextTick(packet) override fun send(packet: OutgoingPacket) = packetsAdapter.sendNextTick(packet)
override suspend fun sendPluginMessage(channel: String, data: ByteBuf) { override suspend fun sendPluginMessage(channel: String, data: ByteBuf) {
if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol") if (this.currentProtocol != PlayProtocol) throw IllegalStateException("The session is not using the PLAY protocol")
send(OutgoingPluginMessagePacket(channel, data)) sendNow(OutgoingPluginMessagePacket(channel, data))
data.release() data.release()
} }
@ -99,8 +103,8 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
} else reason } else reason
when (currentProtocol) { when (currentProtocol) {
LoginProtocol -> send(space.uranos.net.packet.login.DisconnectPacket(finalReason)) LoginProtocol -> sendNow(space.uranos.net.packet.login.DisconnectPacket(finalReason))
PlayProtocol -> send(space.uranos.net.packet.play.DisconnectPacket(finalReason)) PlayProtocol -> sendNow(space.uranos.net.packet.play.DisconnectPacket(finalReason))
} }
} else logger trace "Disconnected" } else logger trace "Disconnected"
@ -110,30 +114,6 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
val joinProcedure = LoginAndJoinProcedure(this) 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 { fun onConnect() = scope.launch {
logger trace "Connected" logger trace "Connected"
packetsAdapter.launchPacketDataChannelConsumer() packetsAdapter.launchPacketDataChannelConsumer()
@ -147,7 +127,7 @@ class UranosSession(val channel: io.netty.channel.Channel, val server: UranosSer
scope.launch { scope.launch {
if (state == State.Disconnected) return@launch if (state == State.Disconnected) return@launch
if (!expected && currentProtocol != HandshakingProtocol && currentProtocol != StatusProtocol) 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() packetsAdapter.stopProcessingIncomingPackets()
coroutineContext.cancel(DisconnectedCancellationException()) 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)) .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 { 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 package space.uranos.net.packet.play
import space.uranos.chat.TextComponent
import space.uranos.net.PacketReceivedEventHandler import space.uranos.net.PacketReceivedEventHandler
import space.uranos.net.UranosSession import space.uranos.net.UranosSession
object IncomingKeepAlivePacketHandler : PacketReceivedEventHandler<IncomingKeepAlivePacket>() { object IncomingKeepAlivePacketHandler : PacketReceivedEventHandler<IncomingKeepAlivePacket>() {
override suspend fun handle(session: UranosSession, packet: IncomingKeepAlivePacket) { override suspend fun handle(session: UranosSession, packet: IncomingKeepAlivePacket) {
if (session.lastKeepAlivePacketTimestamp == packet.id) { val ping = (System.currentTimeMillis() - packet.id).toInt()
session.ping = (System.currentTimeMillis() - session.lastKeepAlivePacketTimestamp).toInt()
session.keepAliveDisconnectJob.cancel() if (ping >= session.server.config.timeout.toMillis()) session.disconnect(TextComponent of "Timed out")
session.scheduleKeepAlivePacket() else session.ping = ping
} else {
session.disconnect(internalReason = "The ID of the last IncomingKeepAlive packet does not match the expected one.")
}
} }
} }

View file

@ -10,7 +10,7 @@ import space.uranos.net.UranosSession
object PlayerOrientationPacketHandler : PacketReceivedEventHandler<PlayerOrientationPacket>() { object PlayerOrientationPacketHandler : PacketReceivedEventHandler<PlayerOrientationPacket>() {
override suspend fun handle(session: UranosSession, packet: 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") ?: error("Player not yet initialized")
} }
} }

View file

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

View file

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