Archived
1
0
Fork 0

Add player movement related packets and work on entities

This commit is contained in:
Moritz Ruth 2021-01-06 18:09:01 +01:00
parent cb50c7bb39
commit 4e34dbc1e2
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
28 changed files with 249 additions and 78 deletions

View file

@ -0,0 +1,8 @@
/*
* 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
class CalledFromWrongThread(message: String) : Exception(message)

View file

@ -35,13 +35,13 @@ interface Scheduler {
*
* If you do not need the return value of [block], you should use [executeAfter] instead.
*/
suspend fun <R : Any> runAfter(delay: Int, block: suspend () -> R): R
suspend fun <R : Any> runAfter(delay: Int, inServerThread: Boolean = false, block: suspend () -> R): R
/**
* Like [runAfter], but the task is *not* cancelled when the current coroutine scope is cancelled.
*/
suspend fun <R : Any> runDetachedAfter(delay: Int, block: suspend () -> R): R =
withContext(NonCancellable) { runAfter(delay, block) }
suspend fun <R : Any> runDetachedAfter(delay: Int, inServerThread: Boolean = false, block: suspend () -> R): R =
withContext(NonCancellable) { runAfter(delay, inServerThread, block) }
interface Task {
fun cancel()

View file

@ -28,6 +28,9 @@ abstract class Entity internal constructor() {
var world: World? = null; private set
suspend fun setWorld(world: World?) {
if (world == null && this is PlayerEntity)
throw IllegalArgumentException("You cannot set the world of a PlayerEntity to null")
if (world == this.world) return
worldMutex.withLock {

View file

@ -9,7 +9,7 @@ import space.uranos.Position
import space.uranos.Vector
abstract class LivingEntity : Entity(), Mobile {
abstract val headPitch: Float
abstract override val position: Position
abstract override val velocity: Vector
abstract var headPitch: Float
abstract override var position: Position
abstract override var velocity: Vector
}

View file

@ -9,10 +9,10 @@ import space.uranos.Position
import space.uranos.Vector
interface Mobile {
val position: Position
var position: Position
/**
* The velocity in blocks per tick.
*/
val velocity: Vector
var velocity: Vector
}

View file

@ -0,0 +1,32 @@
/*
* 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.entity
import space.uranos.Position
import space.uranos.Vector
import space.uranos.player.Player
import space.uranos.world.World
open class PlayerEntity(
override var position: Position,
/**
* The player to which this entity belongs.
*
* **Not every player entity belongs to a player.**
*/
open val player: Player? = null,
override var headPitch: Float = 0f
) : LivingEntity() {
final override val type: EntityType = Type
override var velocity: Vector = Vector.ZERO
/**
* Because [world] is never `null` for player entities, you can use this property instead of writing `world!!`.
*/
val safeWorld: World get() = world!!
companion object Type : PlayerEntityType()
}

View file

@ -19,8 +19,7 @@ import java.util.*
import kotlin.coroutines.CoroutineContext
abstract class Session {
@Suppress("LeakingThis")
val events = EventBusWrapper<Session>(this)
val events by lazy { EventBusWrapper<Session>(this) }
/**
* The IP address of this session
@ -76,6 +75,7 @@ abstract class Session {
val gameMode: GameMode,
val world: World,
val position: Position,
val headPitch: Float,
val invulnerable: Boolean,
val reducedDebugInfo: Boolean,
val selectedHotbarSlot: Int

View file

@ -30,6 +30,8 @@ class SessionAfterLoginEvent(override val target: Session) : SessionEvent(), Can
*/
var initialWorldAndLocation: Pair<World, Position>? = null
var headPitch: Float = 0f
var maxViewDistance: Int = 32
set(value) {
if (value in 2..32) field = value
@ -61,7 +63,7 @@ class SessionAfterLoginEvent(override val target: Session) : SessionEvent(), Can
var reducedDebugInfo: Boolean = false
/**
* See [Player.invulnerable].
* See [Player.vulnerable].
*/
var invulnerable: Boolean = false

View file

@ -5,12 +5,11 @@
package space.uranos.player
import space.uranos.Position
import space.uranos.chat.TextComponent
import space.uranos.entity.PlayerEntity
import space.uranos.net.Session
import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation
import space.uranos.world.World
import java.util.*
import kotlin.coroutines.CoroutineContext
@ -22,6 +21,8 @@ interface Player {
*/
val session: Session
val entity: PlayerEntity
/**
* The name of this player.
*/
@ -51,16 +52,6 @@ interface Player {
val chatMode: ChatMode
)
/**
* The current position of this player.
*/
var position: Position
/**
* The world which currently contains this player.
*/
val world: World
/**
* The index of the hotbar slot which is currently selected.
* Must be in `0..8`.
@ -75,9 +66,9 @@ interface Player {
var reducedDebugInfo: Boolean
/**
* Whether the player cannot take damage through other entities or the world.
* Whether the player can take damage through other entities or the world.
*/
var invulnerable: Boolean
var vulnerable: Boolean
/**
* Whether the player can start flying by itself.

View file

@ -5,6 +5,7 @@
package space.uranos.server
import space.uranos.CalledFromWrongThread
import space.uranos.Registry
import space.uranos.Scheduler
import space.uranos.command.Command
@ -28,6 +29,8 @@ abstract class Server {
abstract val eventBus: EventBus
abstract val eventHandlerPositions: EventHandlerPositionManager
protected abstract val serverThread: Thread
/**
* [CoroutineContext] confined to the server thread.
*
@ -65,6 +68,13 @@ abstract class Server {
*/
abstract fun shutdown()
/**
* Throws [CalledFromWrongThread] when called from a thread which is not the server thread.
*/
fun ensureServerThread(errorMessage: String) {
if (Thread.currentThread() != serverThread) throw CalledFromWrongThread(errorMessage)
}
/**
* Set of all existing [Entity] instances.
*

View file

@ -9,9 +9,9 @@ import io.netty.buffer.ByteBuf
import space.uranos.net.MinecraftProtocolDataTypes.writeVoxelLocation
import space.uranos.net.packet.OutgoingPacketCodec
object SetCompassTargetPacketCodec :
OutgoingPacketCodec<SetCompassTargetPacket>(0x42, SetCompassTargetPacket::class) {
override fun SetCompassTargetPacket.encode(dst: ByteBuf) {
object CompassTargetPacketCodec :
OutgoingPacketCodec<CompassTargetPacket>(0x42, CompassTargetPacket::class) {
override fun CompassTargetPacket.encode(dst: ByteBuf) {
dst.writeVoxelLocation(target)
}
}

View file

@ -0,0 +1,18 @@
/*
* 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.Position
import space.uranos.net.packet.IncomingPacketCodec
object IncomingPlayerPositionPacketCodec :
IncomingPacketCodec<IncomingPlayerPositionPacket>(0x13, IncomingPlayerPositionPacket::class) {
override fun decode(msg: ByteBuf): IncomingPlayerPositionPacket = IncomingPlayerPositionPacket(
Position(msg.readDouble(), msg.readDouble(), msg.readDouble(), 360 - msg.readFloat() % 360, msg.readFloat()),
msg.readBoolean()
)
}

View file

@ -10,9 +10,9 @@ import space.uranos.net.MinecraftProtocolDataTypes.writeVarInt
import space.uranos.net.packet.OutgoingPacketCodec
import space.uranos.util.bitmask
object PlayerPositionAndLookPacketCodec :
OutgoingPacketCodec<PlayerPositionAndLookPacket>(0x34, PlayerPositionAndLookPacket::class) {
override fun PlayerPositionAndLookPacket.encode(dst: ByteBuf) {
object OutgoingPlayerPositionPacketCodec :
OutgoingPacketCodec<OutgoingPlayerPositionPacket>(0x34, OutgoingPlayerPositionPacket::class) {
override fun OutgoingPlayerPositionPacket.encode(dst: ByteBuf) {
dst.writeDouble(position.x)
dst.writeDouble(position.y)
dst.writeDouble(position.z)

View file

@ -12,20 +12,23 @@ object PlayProtocol : Protocol(
ChunkDataPacketCodec,
ChunkLightDataPacketCodec,
ClientSettingsPacketCodec,
CompassTargetPacketCodec,
DeclareCommandsPacketCodec,
DeclareRecipesPacketCodec,
DisconnectPacketCodec,
IncomingKeepAlivePacketCodec,
IncomingPlayerPositionPacketCodec,
IncomingPluginMessagePacketCodec,
JoinGamePacketCodec,
OutgoingKeepAlivePacketCodec,
OutgoingPlayerPositionPacketCodec,
OutgoingPluginMessagePacketCodec,
PlayerAbilitiesPacketCodec,
PlayerInfoPacketCodec,
PlayerPositionAndLookPacketCodec,
PlayerLocationPacketCodec,
PlayerOrientationPacketCodec,
SelectedHotbarSlotPacketCodec,
ServerDifficultyPacketCodec,
SetCompassTargetPacketCodec,
SetSelectedHotbarSlotPacketCodec,
SpawnExperienceOrbPacketCodec,
SpawnLivingEntityPacketCodec,
SpawnObjectEntityPacketCodec,

View file

@ -0,0 +1,18 @@
/*
* 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.Location
import space.uranos.net.packet.IncomingPacketCodec
object PlayerLocationPacketCodec :
IncomingPacketCodec<PlayerLocationPacket>(0x12, PlayerLocationPacket::class) {
override fun decode(msg: ByteBuf): PlayerLocationPacket = PlayerLocationPacket(
Location(msg.readDouble(), msg.readDouble(), msg.readDouble()),
msg.readBoolean()
)
}

View file

@ -0,0 +1,18 @@
/*
* 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.packet.IncomingPacketCodec
object PlayerOrientationPacketCodec :
IncomingPacketCodec<PlayerOrientationPacket>(0x14, PlayerOrientationPacket::class) {
override fun decode(msg: ByteBuf): PlayerOrientationPacket = PlayerOrientationPacket(
360 - msg.readFloat() % 360,
msg.readFloat(),
msg.readBoolean()
)
}

View file

@ -8,9 +8,9 @@ package space.uranos.net.packet.play
import io.netty.buffer.ByteBuf
import space.uranos.net.packet.OutgoingPacketCodec
object SetSelectedHotbarSlotPacketCodec :
OutgoingPacketCodec<SetSelectedHotbarSlotPacket>(0x3F, SetSelectedHotbarSlotPacket::class) {
override fun SetSelectedHotbarSlotPacket.encode(dst: ByteBuf) {
object SelectedHotbarSlotPacketCodec :
OutgoingPacketCodec<SelectedHotbarSlotPacket>(0x3F, SelectedHotbarSlotPacket::class) {
override fun SelectedHotbarSlotPacket.encode(dst: ByteBuf) {
dst.writeByte(index)
}
}

View file

@ -8,4 +8,5 @@ package space.uranos.net.packet.play
import space.uranos.net.packet.OutgoingPacket
import space.uranos.world.VoxelLocation
data class SetCompassTargetPacket(val target: VoxelLocation) : OutgoingPacket()
// TODO: Remove "Set" prefix for all packet names
data class CompassTargetPacket(val target: VoxelLocation) : OutgoingPacket()

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 space.uranos.Position
import space.uranos.net.packet.IncomingPacket
/**
* Combination of [PlayerLocationPacket] and [PlayerOrientationPacket].
*/
data class IncomingPlayerPositionPacket(
val position: Position,
val onGround: Boolean
) : IncomingPacket()

View file

@ -12,7 +12,7 @@ import kotlin.random.Random
/**
* Teleports the receiving player to the specified position.
*/
data class PlayerPositionAndLookPacket(
data class OutgoingPlayerPositionPacket(
val position: Position,
val relativeX: Boolean = false,
val relativeY: Boolean = false,

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 space.uranos.Location
import space.uranos.net.packet.IncomingPacket
/**
* Sent by the client to update the player's x, y and z coordinates on the server.
*/
data class PlayerLocationPacket(
val location: Location,
val onGround: Boolean
) : IncomingPacket()

View file

@ -0,0 +1,26 @@
/*
* 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.Position
import space.uranos.net.packet.IncomingPacket
/**
* Sent by the client to update the player's orientation on the server.
*
* @see [Position]
*/
data class PlayerOrientationPacket(
/**
* Yaw in degrees.
*/
val yaw: Float,
/**
* Pitch in degrees.
*/
val pitch: Float,
val onGround: Boolean
) : IncomingPacket()

View file

@ -10,7 +10,7 @@ import space.uranos.net.packet.OutgoingPacket
/**
* Sent by the server to select a specific hotbar slot.
*/
data class SetSelectedHotbarSlotPacket(val index: Int) : OutgoingPacket() {
data class SelectedHotbarSlotPacket(val index: Int) : OutgoingPacket() {
init {
if (index !in 0..8) throw IllegalArgumentException("index must be between 0 and 8")
}

View file

@ -5,15 +5,13 @@
package space.uranos
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.*
import space.uranos.logging.Logger
import space.uranos.server.Server
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import kotlin.collections.LinkedHashSet
import kotlin.coroutines.resume
@ -21,8 +19,7 @@ import kotlin.coroutines.resume
/**
* Basically ExecutorService but for coroutines and with ticks.
*/
class UranosScheduler : Scheduler {
private lateinit var executor: ScheduledExecutorService
class UranosScheduler(private val executor: ScheduledExecutorService) : Scheduler {
private val tasks = ConcurrentHashMap.newKeySet<Task<out Any>>()
private val shutdownTasks = Collections.synchronizedSet(LinkedHashSet<suspend () -> Unit>())
@ -42,12 +39,13 @@ class UranosScheduler : Scheduler {
}
}
fun startTicking() {
if (this::executor.isInitialized) throw IllegalStateException("Ticking was already started")
private var future: ScheduledFuture<*>? = null
fun start() {
if (future != null) throw IllegalStateException("Already started")
val interval = 1000L / Server.TICKS_PER_SECOND
executor = Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "Scheduler") }
executor.scheduleAtFixedRate({
future = executor.scheduleAtFixedRate({
runBlocking {
val startTime = System.currentTimeMillis()
@ -76,15 +74,14 @@ class UranosScheduler : Scheduler {
}, 0, interval, TimeUnit.MILLISECONDS)
}
private fun stopTicking() {
if (!this::executor.isInitialized) throw IllegalStateException("Ticking was not started")
executor.shutdown()
executor.awaitTermination(3, TimeUnit.SECONDS)
private fun stop() {
if (future == null) throw IllegalArgumentException("Not started")
future!!.cancel(false)
future = null
}
suspend fun shutdown() {
stopTicking()
stop()
shutdownTasks.forEach { it.invoke() }
}
@ -110,12 +107,14 @@ class UranosScheduler : Scheduler {
}
}
override suspend fun <R : Any> runAfter(delay: Int, block: suspend () -> R): R {
// TODO: Use the current coroutine context for the task execution
override suspend fun <R : Any> runAfter(delay: Int, inServerThread: Boolean, block: suspend () -> R): R {
lateinit var continuation: CancellableContinuation<R>
val context = currentCoroutineContext()
val fn = suspend {
continuation.resume(block())
}
val fn =
if (inServerThread) suspend { continuation.resume(block()) }
else suspend { withContext(context) { continuation.resume(block()) } }
val task = Task(fn, null, delay)

View file

@ -19,6 +19,7 @@ import space.uranos.event.UranosEventHandlerPositionManager
import space.uranos.logging.Logger
import space.uranos.logging.UranosLoggingOutputProvider
import space.uranos.net.UranosSocketServer
import space.uranos.player.UranosPlayer
import space.uranos.plugin.UranosPluginManager
import space.uranos.recipe.Recipe
import space.uranos.server.Server
@ -28,6 +29,7 @@ import space.uranos.world.Dimension
import java.io.File
import java.security.KeyPair
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
@ -46,12 +48,15 @@ class UranosServer internal constructor() : Server() {
val x509EncodedPublicKey: ByteArray = EncryptionUtils.generateX509Key(keyPair.public).encoded
override lateinit var serverThread: Thread
private val scheduledExecutorService: ScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor { r -> Thread(r, "server").also { serverThread = it } }
override val coroutineContext: CoroutineContext =
CoroutineName("Server") + SupervisorJob() +
Executors.newSingleThreadExecutor { r -> Thread(r, "server") }.asCoroutineDispatcher()
CoroutineName("Server") + SupervisorJob() + scheduledExecutorService.asCoroutineDispatcher()
override val sessions by socketServer::sessions
override val players get() = sessions.mapNotNull { it.player }
override val players get() = sessions.mapNotNull { it.player as UranosPlayer? }
override val pluginManager = UranosPluginManager(this)
override val serverDirectory: File =
@ -67,7 +72,7 @@ class UranosServer internal constructor() : Server() {
override val biomeRegistry = BiomeRegistry()
override val loggingOutputProvider = UranosLoggingOutputProvider
override val scheduler = UranosScheduler()
override val scheduler = UranosScheduler(scheduledExecutorService)
val config = ConfigLoader.Builder()
.addPropertySource(
@ -110,7 +115,7 @@ class UranosServer internal constructor() : Server() {
socketServer.bind()
logger info "Listening on ${config.host}:${config.port}"
scheduler.startTicking()
scheduler.start()
}
companion object {

View file

@ -135,6 +135,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
event.gameMode,
initialWorldAndLocation.first,
initialWorldAndLocation.second,
event.headPitch,
event.invulnerable,
event.reducedDebugInfo,
event.selectedHotbarSlot
@ -160,8 +161,8 @@ class LoginAndJoinProcedure(val session: UranosSession) {
state.uuid,
state.gameMode,
settings,
state.world,
state.position,
state.headPitch,
state.reducedDebugInfo,
state.fieldOfView,
state.canFly,
@ -174,7 +175,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
session.state = Session.State.Joining(player)
session.send(SetSelectedHotbarSlotPacket(state.selectedHotbarSlot))
session.send(SelectedHotbarSlotPacket(state.selectedHotbarSlot))
session.send(DeclareRecipesPacket(session.server.recipeRegistry.items.values))
session.send(tagsPacket)
@ -182,7 +183,7 @@ class LoginAndJoinProcedure(val session: UranosSession) {
// session.send(DeclareCommandsPacket(session.server.commandRegistry.items.values))
// UnlockRecipes
session.send(PlayerPositionAndLookPacket(state.position))
session.send(OutgoingPlayerPositionPacket(state.position))
session.send(PlayerInfoPacket(PlayerInfoPacket.Action.AddPlayer((session.server.players + player).map {
it.uuid to PlayerInfoPacket.Action.AddPlayer.Data(
@ -203,14 +204,14 @@ class LoginAndJoinProcedure(val session: UranosSession) {
)
)
session.send(UpdateViewPositionPacket(Chunk.Key.from(player.position.toVoxelLocation())))
session.send(UpdateViewPositionPacket(Chunk.Key.from(player.entity.position.toVoxelLocation())))
session.scheduleKeepAlivePacket(true)
player.sendChunksAndLight()
// WorldBorder
session.send(SetCompassTargetPacket(player.compassTarget))
session.send(PlayerPositionAndLookPacket(state.position))
session.send(CompassTargetPacket(player.compassTarget))
session.send(OutgoingPlayerPositionPacket(state.position))
// TODO: Wait for ClientStatus(action=0) packet
}

View file

@ -140,7 +140,7 @@ class UranosSession(private val channel: io.netty.channel.Channel, val server: U
val codec = currentProtocol!!.incomingPacketCodecsByID[packetID]
if (codec == null) {
val message = "Received an unknown packet (ID: $packetID)"
val message = "Received an unknown packet (ID: 0x${packetID.toString(16).padStart(2, '0')})"
if (server.config.developmentMode)
logger warn "$message. This will cause the client to disconnect in production mode."

View file

@ -7,13 +7,13 @@ package space.uranos.player
import space.uranos.Position
import space.uranos.chat.TextComponent
import space.uranos.entity.PlayerEntity
import space.uranos.net.Session
import space.uranos.net.packet.play.ChunkDataPacket
import space.uranos.net.packet.play.ChunkLightDataPacket
import space.uranos.util.clampArgument
import space.uranos.world.Chunk
import space.uranos.world.VoxelLocation
import space.uranos.world.World
import java.util.*
import kotlin.math.abs
@ -23,14 +23,14 @@ class UranosPlayer(
override val uuid: UUID,
override var gameMode: GameMode,
override var settings: Player.Settings,
override val world: World,
override var position: Position,
position: Position,
headPitch: Float,
override var reducedDebugInfo: Boolean,
override var fieldOfView: Float,
override var canFly: Boolean,
override var flying: Boolean,
override var flyingSpeed: Float,
override var invulnerable: Boolean,
override var vulnerable: Boolean,
override var compassTarget: VoxelLocation,
selectedHotbarSlot: Int
) : Player {
@ -51,18 +51,20 @@ class UranosPlayer(
updateCurrentlyViewedChunks()
}
override val entity: PlayerEntity = PlayerEntity(position, this, headPitch)
/**
* Sets [currentlyViewedChunks] to all chunks in the view distance.
*/
private fun updateCurrentlyViewedChunks() {
val (centerX, centerZ) = Chunk.Key.from(position.toVoxelLocation())
val (centerX, centerZ) = Chunk.Key.from(entity.position.toVoxelLocation())
val edgeLength = settings.viewDistance + 1
currentlyViewedChunks = buildList(edgeLength * edgeLength) {
for (x in (centerX - settings.viewDistance)..(centerX + settings.viewDistance)) {
for (z in (centerZ - settings.viewDistance)..(centerZ + settings.viewDistance)) {
add(world.getChunk(Chunk.Key(x, z)))
add(entity.safeWorld.getChunk(Chunk.Key(x, z)))
}
}
}