diff --git a/README.md b/README.md index f3b0ea3..dc67270 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt b/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt index 013d96d..8a1b056 100644 --- a/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt +++ b/test-plugin/src/main/kotlin/space/uranos/testplugin/TestPlugin.kt @@ -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 } } diff --git a/uranos-api/src/main/kotlin/space/uranos/Position.kt b/uranos-api/src/main/kotlin/space/uranos/Position.kt index 7028905..f2b49ba 100644 --- a/uranos-api/src/main/kotlin/space/uranos/Position.kt +++ b/uranos-api/src/main/kotlin/space/uranos/Position.kt @@ -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 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 diff --git a/uranos-api/src/main/kotlin/space/uranos/net/Session.kt b/uranos-api/src/main/kotlin/space/uranos/net/Session.kt index f5b2499..af54a2b 100644 --- a/uranos-api/src/main/kotlin/space/uranos/net/Session.kt +++ b/uranos-api/src/main/kotlin/space/uranos/net/Session.kt @@ -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(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. diff --git a/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt b/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt index a026ce3..6a87de9 100644 --- a/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt +++ b/uranos-api/src/main/kotlin/space/uranos/util/MemoizedDelegate.kt @@ -7,14 +7,14 @@ package space.uranos.util import kotlin.reflect.KProperty -class MemoizedDelegate(private val dependingGetter: () -> Any?, private val initializer: () -> T) { +class MemoizedDelegate(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() diff --git a/uranos-api/src/main/kotlin/space/uranos/world/VoxelLocation.kt b/uranos-api/src/main/kotlin/space/uranos/world/VoxelLocation.kt index 63c6d55..4a92669 100644 --- a/uranos-api/src/main/kotlin/space/uranos/world/VoxelLocation.kt +++ b/uranos-api/src/main/kotlin/space/uranos/world/VoxelLocation.kt @@ -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()) diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityOrientationPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityHeadPitchPacketCodec.kt similarity index 62% rename from uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityOrientationPacketCodec.kt rename to uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityHeadPitchPacketCodec.kt index 9f70490..30daa1a 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityOrientationPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityHeadPitchPacketCodec.kt @@ -9,11 +9,11 @@ import io.netty.buffer.ByteBuf import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt import space.uranos.net.packet.OutgoingPacketCodec -object EntityOrientationPacketCodec : - OutgoingPacketCodec(0x29, EntityOrientationPacket::class) { - override fun EntityOrientationPacket.encode(dst: ByteBuf) { +object EntityHeadPitchPacketCodec : + OutgoingPacketCodec(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) } diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityHeadYawPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityHeadYawPacketCodec.kt new file mode 100644 index 0000000..0b0d1a2 --- /dev/null +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityHeadYawPacketCodec.kt @@ -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(0x3A, EntityHeadYawPacket::class) { + override fun EntityHeadYawPacket.encode(dst: ByteBuf) { + dst.writeVarInt(entityID) + dst.writeByte(yaw.toInt()) + } +} diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacketCodec.kt index d6fc32a..c2ded60 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacketCodec.kt @@ -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) } } diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacketCodec.kt index f1bf5b9..70d7826 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacketCodec.kt @@ -13,11 +13,11 @@ object EntityTeleportPacketCodec : OutgoingPacketCodec(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) } } diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/OutgoingPlayerPositionPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/OutgoingPlayerPositionPacketCodec.kt index e1e3a4b..bea01bd 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/OutgoingPlayerPositionPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/OutgoingPlayerPositionPacketCodec.kt @@ -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 } diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt index f26f784..2944ea2 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/PlayProtocol.kt @@ -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 ) diff --git a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt index 4d8d169..284e25a 100644 --- a/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt +++ b/uranos-packet-codecs/src/main/kotlin/space/uranos/net/packet/play/SpawnLivingEntityPacketCodec.kt @@ -20,7 +20,7 @@ object SpawnLivingEntityPacketCodec : OutgoingPacketCodec(0x40, UpdateViewPositionPacket::class) { - override fun UpdateViewPositionPacket.encode(dst: ByteBuf) { +object ViewPositionPacketCodec : + OutgoingPacketCodec(0x40, ViewPositionPacket::class) { + override fun ViewPositionPacket.encode(dst: ByteBuf) { dst.writeVarInt(chunkKey.x) dst.writeVarInt(chunkKey.z) } diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityOrientationPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityHeadPitchPacket.kt similarity index 86% rename from uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityOrientationPacket.kt rename to uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityHeadPitchPacket.kt index 5363f37..fe29c82 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityOrientationPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityHeadPitchPacket.kt @@ -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() diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityHeadYawPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityHeadYawPacket.kt new file mode 100644 index 0000000..f6b37b8 --- /dev/null +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityHeadYawPacket.kt @@ -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() diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacket.kt index 1224f3b..47b9f47 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityRelativeMoveWithOrientationPacket.kt @@ -19,6 +19,6 @@ data class EntityRelativeMoveWithOrientationPacket( /** * Absolute value. */ - val pitch: UByte, + val headPitch: UByte, val onGround: Boolean ) : OutgoingPacket() diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacket.kt index 7cddffd..3170f1f 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/EntityTeleportPacket.kt @@ -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 + ) +} diff --git a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/UpdateViewPositionPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/ViewPositionPacket.kt similarity index 85% rename from uranos-packets/src/main/kotlin/space/uranos/net/packet/play/UpdateViewPositionPacket.kt rename to uranos-packets/src/main/kotlin/space/uranos/net/packet/play/ViewPositionPacket.kt index a23a0f4..6797f9e 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/UpdateViewPositionPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/net/packet/play/ViewPositionPacket.kt @@ -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() diff --git a/uranos-packets/src/main/kotlin/space/uranos/util/CreateEntityMovementPacket.kt b/uranos-packets/src/main/kotlin/space/uranos/util/CreateEntityMovementPacket.kt index 96610b3..0a25968 100644 --- a/uranos-packets/src/main/kotlin/space/uranos/util/CreateEntityMovementPacket.kt +++ b/uranos-packets/src/main/kotlin/space/uranos/util/CreateEntityMovementPacket.kt @@ -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, diff --git a/uranos-server/src/main/kotlin/space/uranos/UranosScheduler.kt b/uranos-server/src/main/kotlin/space/uranos/UranosScheduler.kt index 5c9f316..762ec2a 100644 --- a/uranos-server/src/main/kotlin/space/uranos/UranosScheduler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/UranosScheduler.kt @@ -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 diff --git a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt index 0530105..998591d 100644 --- a/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/UranosServer.kt @@ -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(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))) } } diff --git a/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt b/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt index b4f2021..cbd642f 100644 --- a/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt +++ b/uranos-server/src/main/kotlin/space/uranos/entity/UranosEntity.kt @@ -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) } } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/LoginAndJoinProcedure.kt b/uranos-server/src/main/kotlin/space/uranos/net/LoginAndJoinProcedure.kt index 13bc697..ea1e4f0 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/LoginAndJoinProcedure.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/LoginAndJoinProcedure.kt @@ -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)) } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt b/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt index 395d107..d5e45aa 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/PacketsAdapter.kt @@ -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 } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt b/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt index 5e4ea44..c4d5fab 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/UranosSession.kt @@ -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() - 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) } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingKeepAlivePacketHandler.kt b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingKeepAlivePacketHandler.kt index 4cb2869..21483a3 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingKeepAlivePacketHandler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/IncomingKeepAlivePacketHandler.kt @@ -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() { 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 } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt index 652ea2f..a4350e4 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/packet/play/PlayerOrientationPacketHandler.kt @@ -10,7 +10,7 @@ import space.uranos.net.UranosSession object PlayerOrientationPacketHandler : PacketReceivedEventHandler() { 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") } } diff --git a/uranos-server/src/main/kotlin/space/uranos/net/packet/status/StatusProtocolHandler.kt b/uranos-server/src/main/kotlin/space/uranos/net/packet/status/StatusProtocolHandler.kt index 97f7c79..42a9a70 100644 --- a/uranos-server/src/main/kotlin/space/uranos/net/packet/status/StatusProtocolHandler.kt +++ b/uranos-server/src/main/kotlin/space/uranos/net/packet/status/StatusProtocolHandler.kt @@ -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 { session, packet -> - session.send(PongPacket(packet.payload)) + session.sendNow(PongPacket(packet.payload)) session.disconnect() } )) diff --git a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt index 5b8c4a6..c43b246 100644 --- a/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt +++ b/uranos-server/src/main/kotlin/space/uranos/player/UranosPlayer.kt @@ -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 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))) } } }