Add more join procedure logic and packets
This commit is contained in:
parent
61d9ec8d2b
commit
f80ee719f4
40 changed files with 678 additions and 92 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
# Project exclude paths
|
||||
/.gradle/
|
||||
/build/
|
||||
/blokk-api/build/
|
||||
/blokk-server/build/
|
||||
/data
|
||||
|
|
8
blokk-api/src/main/kotlin/space/blokk/GameMode.kt
Normal file
8
blokk-api/src/main/kotlin/space/blokk/GameMode.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package space.blokk
|
||||
|
||||
enum class GameMode {
|
||||
SURVIVAL,
|
||||
ADVENTURE,
|
||||
CREATIVE,
|
||||
SPECTATOR
|
||||
}
|
|
@ -2,4 +2,24 @@ package space.blokk.events
|
|||
|
||||
interface EventTarget<T : Event> {
|
||||
val eventBus: EventBus<T>
|
||||
|
||||
/**
|
||||
* Shorthand for [`eventBus.emit(event)`][EventBus.emit].
|
||||
*/
|
||||
suspend fun emit(event: T) = eventBus.emit(event)
|
||||
|
||||
/**
|
||||
* Shorthand for [`eventBus.emitAsync(event)`][EventBus.emitAsync].
|
||||
*/
|
||||
fun emitAsync(event: T) = eventBus.emitAsync(event)
|
||||
|
||||
/**
|
||||
* Shorthand for [`eventBus.register(listener)`][EventBus.register].
|
||||
*/
|
||||
fun registerListener(listener: Listener) = eventBus.register(listener)
|
||||
|
||||
/**
|
||||
* Shorthand for [`eventBus.unregister(listener)`][EventBus.unregister].
|
||||
*/
|
||||
fun unregisterListener(listener: Listener) = eventBus.unregister(listener)
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ object MinecraftDataTypes {
|
|||
*
|
||||
* @see <a href="https://wiki.vg/Protocol#VarInt_and_VarLong">https://wiki.vg/Protocol#VarInt_and_VarLong</a>
|
||||
*/
|
||||
fun ByteBuf.writeVarInt(value: Int) {
|
||||
fun ByteBuf.writeVarInt(value: Int): ByteBuf {
|
||||
var v = value
|
||||
var part: Byte
|
||||
while (true) {
|
||||
|
@ -74,6 +74,8 @@ object MinecraftDataTypes {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,9 +97,11 @@ object MinecraftDataTypes {
|
|||
*
|
||||
* @see <a href="https://wiki.vg/Protocol#Data_types">https://wiki.vg/Protocol#Data_types</a>
|
||||
*/
|
||||
fun ByteBuf.writeString(value: String) {
|
||||
fun ByteBuf.writeString(value: String): ByteBuf {
|
||||
val bytes = value.toByteArray(Charsets.UTF_8)
|
||||
writeVarInt(bytes.size)
|
||||
writeBytes(bytes)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package space.blokk.net
|
||||
|
||||
class PacketDecodingException : Exception("Packet decoding failed")
|
|
@ -1,5 +1,6 @@
|
|||
package space.blokk.net
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import space.blokk.events.EventTarget
|
||||
import space.blokk.net.events.SessionEvent
|
||||
|
@ -11,9 +12,31 @@ interface Session : EventTarget<SessionEvent> {
|
|||
/**
|
||||
* The protocol this session is currently using.
|
||||
*/
|
||||
var currentProtocol: Protocol
|
||||
val currentProtocol: Protocol
|
||||
|
||||
/**
|
||||
* The IP address of this session
|
||||
*/
|
||||
val address: InetAddress
|
||||
|
||||
/**
|
||||
* The coroutine scope of this session. It is cancelled when the session disconnects.
|
||||
*/
|
||||
val scope: CoroutineScope
|
||||
|
||||
/**
|
||||
* Sends a packet.
|
||||
*/
|
||||
suspend fun send(packet: OutgoingPacket)
|
||||
|
||||
/**
|
||||
* Sends a plugin message packet.
|
||||
*/
|
||||
suspend fun sendPluginMessage(channel: String, data: ByteBuf)
|
||||
|
||||
/**
|
||||
* The brand name the client optionally sent during the login procedure.
|
||||
* [ClientBrandReceivedEvent][space.blokk.net.events.ClientBrandReceivedEvent] is emitted when this value changes.
|
||||
*/
|
||||
val brand: String?
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package space.blokk.net.events
|
||||
|
||||
import space.blokk.net.Session
|
||||
|
||||
/**
|
||||
* Emitted when the client sends his brand during the login process.
|
||||
*/
|
||||
class ClientBrandReceivedEvent(
|
||||
session: Session,
|
||||
val brand: String
|
||||
) : SessionEvent(session)
|
|
@ -4,6 +4,9 @@ import space.blokk.events.Cancellable
|
|||
import space.blokk.net.Session
|
||||
import space.blokk.net.protocols.IncomingPacket
|
||||
|
||||
/**
|
||||
* Emitted when a packet is received. **You should only listen to this event when there is no other option.**
|
||||
*/
|
||||
class PacketReceivedEvent<T : IncomingPacket>(session: Session, var packet: T) : SessionEvent(session), Cancellable {
|
||||
override var isCancelled = false
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ import space.blokk.events.Cancellable
|
|||
import space.blokk.net.Session
|
||||
import space.blokk.net.protocols.OutgoingPacket
|
||||
|
||||
/**
|
||||
* Emitted when a packet is going to be sent. **You should only listen to this event when there is no other option.**
|
||||
*/
|
||||
class PacketSendEvent(session: Session, var packet: OutgoingPacket) : SessionEvent(session), Cancellable {
|
||||
override var isCancelled = false
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ import space.blokk.events.Cancellable
|
|||
import space.blokk.net.Session
|
||||
import space.blokk.net.protocols.status.ResponsePacket
|
||||
|
||||
/**
|
||||
* Emitted when a client requests general info about the server, most likely to show it in the server list.
|
||||
*/
|
||||
class ServerListInfoRequestEvent(
|
||||
session: Session,
|
||||
var response: ResponsePacket
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package space.blokk.net.protocols
|
||||
|
||||
abstract class Protocol internal constructor(val name: String, packets: Set<PacketCompanion<*>>) {
|
||||
abstract class Protocol internal constructor(val name: String, vararg packets: PacketCompanion<*>) {
|
||||
val incomingPackets = packets.filterIsInstance<IncomingPacketCompanion<*>>()
|
||||
val incomingPacketsByID = incomingPackets.mapToIDMap()
|
||||
val outgoingPackets = packets.filterIsInstance<OutgoingPacketCompanion<*>>()
|
||||
|
|
|
@ -2,4 +2,4 @@ package space.blokk.net.protocols.handshaking
|
|||
|
||||
import space.blokk.net.protocols.Protocol
|
||||
|
||||
object HandshakingProtocol : Protocol("HANDSHAKING", setOf(HandshakePacket))
|
||||
object HandshakingProtocol : Protocol("HANDSHAKING", HandshakePacket)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package space.blokk.net.protocols.login
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftDataTypes.writeString
|
||||
import space.blokk.net.MinecraftDataTypes.writeVarInt
|
||||
import space.blokk.net.protocols.OutgoingPacket
|
||||
import space.blokk.net.protocols.OutgoingPacketCompanion
|
||||
|
@ -16,11 +17,33 @@ import space.blokk.net.protocols.OutgoingPacketCompanion
|
|||
data class LoginPluginRequestPacket(
|
||||
val messageID: Int,
|
||||
val channel: String,
|
||||
val data: ByteBuf
|
||||
val data: ByteArray
|
||||
) : OutgoingPacket() {
|
||||
companion object : OutgoingPacketCompanion<LoginPluginRequestPacket>(0x04, LoginPluginRequestPacket::class)
|
||||
|
||||
override fun encode(dst: ByteBuf) {
|
||||
dst.writeVarInt(messageID)
|
||||
dst.writeString(channel)
|
||||
dst.writeBytes(data)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as LoginPluginRequestPacket
|
||||
|
||||
if (messageID != other.messageID) return false
|
||||
if (channel != other.channel) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = messageID
|
||||
result = 31 * result + channel.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ package space.blokk.net.protocols.login
|
|||
import space.blokk.net.protocols.Protocol
|
||||
|
||||
object LoginProtocol : Protocol(
|
||||
"LOGIN", setOf(
|
||||
DisconnectPacket,
|
||||
LoginStartPacket,
|
||||
EncryptionRequestPacket,
|
||||
EncryptionResponsePacket,
|
||||
SetCompressionPacket,
|
||||
LoginSuccessPacket,
|
||||
LoginPluginRequestPacket,
|
||||
LoginPluginResponsePacket
|
||||
)
|
||||
"LOGIN",
|
||||
DisconnectPacket,
|
||||
LoginStartPacket,
|
||||
EncryptionRequestPacket,
|
||||
EncryptionResponsePacket,
|
||||
SetCompressionPacket,
|
||||
LoginSuccessPacket,
|
||||
LoginPluginRequestPacket,
|
||||
LoginPluginResponsePacket
|
||||
)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftDataTypes
|
||||
import space.blokk.net.PacketDecodingException
|
||||
import space.blokk.net.protocols.IncomingPacket
|
||||
import space.blokk.net.protocols.IncomingPacketCompanion
|
||||
import space.blokk.players.ChatMode
|
||||
import space.blokk.players.DisplayedSkinParts
|
||||
import space.blokk.players.MainHand
|
||||
import space.blokk.utils.checkBit
|
||||
|
||||
/**
|
||||
* Sent by the client when the player joins or the settings changed.
|
||||
*/
|
||||
data class ClientSettingsPacket(
|
||||
val locale: String,
|
||||
val viewDistance: Byte,
|
||||
val chatMode: ChatMode,
|
||||
val chatColorsEnabled: Boolean,
|
||||
val displayedSkinParts: DisplayedSkinParts,
|
||||
val mainHand: MainHand
|
||||
) : IncomingPacket() {
|
||||
companion object : IncomingPacketCompanion<ClientSettingsPacket>(0x05, ClientSettingsPacket::class) {
|
||||
override fun decode(msg: ByteBuf): ClientSettingsPacket {
|
||||
return with(MinecraftDataTypes) {
|
||||
val locale = msg.readString()
|
||||
val viewDistance = msg.readByte()
|
||||
|
||||
val chatMode = when (msg.readVarInt()) {
|
||||
0 -> ChatMode.ENABLED
|
||||
1 -> ChatMode.COMMANDS_ONLY
|
||||
2 -> ChatMode.HIDDEN
|
||||
else -> throw PacketDecodingException()
|
||||
}
|
||||
|
||||
val chatColorsEnabled = msg.readBoolean()
|
||||
|
||||
val skinFlags = msg.readByte()
|
||||
val displayedSkinParts = DisplayedSkinParts(
|
||||
cape = skinFlags.checkBit(0),
|
||||
jacket = skinFlags.checkBit(1),
|
||||
leftSleeve = skinFlags.checkBit(2),
|
||||
rightSleeve = skinFlags.checkBit(3),
|
||||
leftPantsLeg = skinFlags.checkBit(4),
|
||||
rightPantsLeg = skinFlags.checkBit(5),
|
||||
hat = skinFlags.checkBit(6)
|
||||
)
|
||||
|
||||
val mainHand = when (msg.readVarInt()) {
|
||||
0 -> MainHand.LEFT
|
||||
1 -> MainHand.RIGHT
|
||||
else -> throw PacketDecodingException()
|
||||
}
|
||||
|
||||
ClientSettingsPacket(
|
||||
locale,
|
||||
viewDistance,
|
||||
chatMode,
|
||||
chatColorsEnabled,
|
||||
displayedSkinParts,
|
||||
mainHand
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftDataTypes
|
||||
import space.blokk.net.protocols.IncomingPacket
|
||||
import space.blokk.net.protocols.IncomingPacketCompanion
|
||||
|
||||
/**
|
||||
* Can be used by client-side mods to communicate with a potentially modded server.
|
||||
* Minecraft itself [also uses this for some things](https://wiki.vg/Plugin_channels#Channels_internal_to_Minecraft).
|
||||
*
|
||||
* @param channel Channel in which the message was sent.
|
||||
* @param data Data which was sent.
|
||||
*/
|
||||
data class IncomingPluginMessagePacket(val channel: String, val data: ByteArray) : IncomingPacket() {
|
||||
companion object : IncomingPacketCompanion<IncomingPluginMessagePacket>(0x0B, IncomingPluginMessagePacket::class) {
|
||||
override fun decode(msg: ByteBuf): IncomingPluginMessagePacket {
|
||||
return with(MinecraftDataTypes) {
|
||||
val channel = msg.readString()
|
||||
|
||||
// TODO: Test this
|
||||
val data = ByteArray(msg.readableBytes()).also { msg.readBytes(it) }
|
||||
|
||||
IncomingPluginMessagePacket(channel, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as IncomingPluginMessagePacket
|
||||
|
||||
if (channel != other.channel) return false
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = channel.hashCode()
|
||||
result = 31 * result + data.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.GameMode
|
||||
import space.blokk.net.MinecraftDataTypes.writeString
|
||||
import space.blokk.net.MinecraftDataTypes.writeVarInt
|
||||
import space.blokk.net.protocols.OutgoingPacket
|
||||
import space.blokk.net.protocols.OutgoingPacketCompanion
|
||||
import space.blokk.worlds.WorldDimension
|
||||
import space.blokk.worlds.WorldType
|
||||
|
||||
/**
|
||||
* Sent by the server after the client logged in.
|
||||
*
|
||||
* @param entityID ID of the player entity.
|
||||
* @param gameMode Game mode of the player.
|
||||
* @param hardcore Whether the player is in hardcore mode.
|
||||
* @param worldDimension Dimension of the world the player joins.
|
||||
* @param worldSeedHash First 8 bytes of the SHA-256 hash of the world's seed.
|
||||
* @param worldType Type of the world the player joins.
|
||||
* @param viewDistance Maximum view distance.
|
||||
* @param reducedDebugInfo Whether the debug screen shows only reduced info.
|
||||
* @param respawnScreenEnabled Whether the respawn screen is shown when the player dies.
|
||||
*/
|
||||
data class JoinGamePacket(
|
||||
val entityID: Int,
|
||||
val gameMode: GameMode,
|
||||
val hardcore: Boolean,
|
||||
val worldDimension: WorldDimension,
|
||||
val worldSeedHash: Long,
|
||||
val worldType: WorldType,
|
||||
val viewDistance: Byte,
|
||||
val reducedDebugInfo: Boolean,
|
||||
val respawnScreenEnabled: Boolean
|
||||
) : OutgoingPacket() {
|
||||
companion object : OutgoingPacketCompanion<JoinGamePacket>(0x26, JoinGamePacket::class)
|
||||
|
||||
init {
|
||||
if (viewDistance < 2 || viewDistance > 32) throw IllegalArgumentException("viewDistance must be between 2 and 32.")
|
||||
}
|
||||
|
||||
override fun encode(dst: ByteBuf) {
|
||||
dst.writeInt(entityID)
|
||||
var gameMode = when (gameMode) {
|
||||
GameMode.SURVIVAL -> 0
|
||||
GameMode.CREATIVE -> 1
|
||||
GameMode.ADVENTURE -> 2
|
||||
GameMode.SPECTATOR -> 3
|
||||
}
|
||||
|
||||
if (hardcore) gameMode = gameMode and 0x8
|
||||
|
||||
dst.writeByte(gameMode)
|
||||
|
||||
dst.writeInt(
|
||||
when (worldDimension) {
|
||||
WorldDimension.NETHER -> -1
|
||||
WorldDimension.OVERWORLD -> 0
|
||||
WorldDimension.END -> 1
|
||||
}
|
||||
)
|
||||
|
||||
dst.writeLong(worldSeedHash)
|
||||
dst.writeByte(0x00) // max players; not used anymore
|
||||
|
||||
dst.writeString(
|
||||
when (worldType) {
|
||||
WorldType.DEFAULT -> "default"
|
||||
WorldType.FLAT -> "flat"
|
||||
WorldType.LARGE_BIOMES -> "largeBiomes"
|
||||
WorldType.AMPLIFIED -> "amplified"
|
||||
WorldType.CUSTOMIZED -> "customized"
|
||||
WorldType.BUFFET -> "buffet"
|
||||
}
|
||||
)
|
||||
|
||||
dst.writeVarInt(viewDistance.toInt())
|
||||
dst.writeBoolean(reducedDebugInfo)
|
||||
dst.writeBoolean(respawnScreenEnabled)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.MinecraftDataTypes.writeString
|
||||
import space.blokk.net.protocols.OutgoingPacket
|
||||
import space.blokk.net.protocols.OutgoingPacketCompanion
|
||||
|
||||
/**
|
||||
* Can be used by mods or plugins to communicate with a potentially modded client.
|
||||
* Minecraft itself [also uses this for some things](https://wiki.vg/Plugin_channels#Channels_internal_to_Minecraft).
|
||||
*
|
||||
* @param channel Channel in which the message should be send.
|
||||
* @param data Data which should be send. The ByteBuf needs to be released by the caller.
|
||||
*/
|
||||
data class OutgoingPluginMessagePacket(val channel: String, val data: ByteBuf) : OutgoingPacket() {
|
||||
companion object : OutgoingPacketCompanion<OutgoingPluginMessagePacket>(0x19, OutgoingPluginMessagePacket::class)
|
||||
|
||||
override fun encode(dst: ByteBuf) {
|
||||
dst.writeString(channel)
|
||||
dst.writeBytes(data)
|
||||
}
|
||||
}
|
|
@ -2,4 +2,12 @@ package space.blokk.net.protocols.play
|
|||
|
||||
import space.blokk.net.protocols.Protocol
|
||||
|
||||
object PlayProtocol : Protocol("PLAY", setOf())
|
||||
object PlayProtocol : Protocol(
|
||||
"PLAY",
|
||||
JoinGamePacket,
|
||||
ClientSettingsPacket,
|
||||
IncomingPluginMessagePacket,
|
||||
OutgoingPluginMessagePacket,
|
||||
PlayerAbilitiesPacket,
|
||||
ServerDifficultyPacket
|
||||
)
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.protocols.OutgoingPacket
|
||||
import space.blokk.net.protocols.OutgoingPacketCompanion
|
||||
|
||||
/**
|
||||
* Tells the client some things about the player.
|
||||
*
|
||||
* @param invulnerable Whether the player is invulnerable.
|
||||
* @param flying Whether the player is currently flying.
|
||||
* @param flyingAllowed Whether the player is allowed to fly.
|
||||
* @param instantlyBreakBlocks Whether can break blocks instantly like in creative mode.
|
||||
* @param flyingSpeed The player's flying speed. The default value is `0.5`
|
||||
* @param fieldOfView The player's field of view modifier. The default value is `0.1`.
|
||||
*/
|
||||
data class PlayerAbilitiesPacket(
|
||||
val invulnerable: Boolean,
|
||||
val flying: Boolean,
|
||||
val flyingAllowed: Boolean,
|
||||
val instantlyBreakBlocks: Boolean,
|
||||
val flyingSpeed: Float = 0.5f,
|
||||
val fieldOfView: Float = 0.1f
|
||||
) : OutgoingPacket() {
|
||||
companion object : OutgoingPacketCompanion<PlayerAbilitiesPacket>(0x32, PlayerAbilitiesPacket::class)
|
||||
|
||||
override fun encode(dst: ByteBuf) {
|
||||
var flags = 0
|
||||
if (invulnerable) flags = flags and 0x01
|
||||
if (flying) flags = flags and 0x02
|
||||
if (flyingAllowed) flags = flags and 0x04
|
||||
if (instantlyBreakBlocks) flags = flags and 0x08
|
||||
|
||||
dst.writeByte(flags)
|
||||
dst.writeFloat(flyingSpeed)
|
||||
dst.writeFloat(fieldOfView)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import space.blokk.net.protocols.OutgoingPacket
|
||||
import space.blokk.net.protocols.OutgoingPacketCompanion
|
||||
import space.blokk.worlds.WorldDifficulty
|
||||
|
||||
/**
|
||||
* Sets the difficulty shown in the pause menu.
|
||||
*/
|
||||
data class ServerDifficultyPacket(val difficulty: WorldDifficulty, val locked: Boolean) : OutgoingPacket() {
|
||||
companion object : OutgoingPacketCompanion<ServerDifficultyPacket>(0x0E, ServerDifficultyPacket::class)
|
||||
|
||||
override fun encode(dst: ByteBuf) {
|
||||
dst.writeByte(
|
||||
when (difficulty) {
|
||||
WorldDifficulty.PEACEFUL -> 0
|
||||
WorldDifficulty.EASY -> 1
|
||||
WorldDifficulty.NORMAL -> 2
|
||||
WorldDifficulty.HARD -> 3
|
||||
}
|
||||
)
|
||||
|
||||
dst.writeBoolean(locked)
|
||||
}
|
||||
}
|
|
@ -2,4 +2,10 @@ package space.blokk.net.protocols.status
|
|||
|
||||
import space.blokk.net.protocols.Protocol
|
||||
|
||||
object StatusProtocol : Protocol("STATUS", setOf(RequestPacket, ResponsePacket, PingPacket, PongPacket))
|
||||
object StatusProtocol : Protocol(
|
||||
"STATUS",
|
||||
RequestPacket,
|
||||
ResponsePacket,
|
||||
PingPacket,
|
||||
PongPacket
|
||||
)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package space.blokk.players
|
||||
|
||||
enum class ChatMode {
|
||||
ENABLED,
|
||||
COMMANDS_ONLY,
|
||||
HIDDEN
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package space.blokk.players
|
||||
|
||||
data class DisplayedSkinParts(
|
||||
val cape: Boolean,
|
||||
val jacket: Boolean,
|
||||
val leftSleeve: Boolean,
|
||||
val rightSleeve: Boolean,
|
||||
val leftPantsLeg: Boolean,
|
||||
val rightPantsLeg: Boolean,
|
||||
val hat: Boolean
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package space.blokk.players
|
||||
|
||||
enum class MainHand {
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
9
blokk-api/src/main/kotlin/space/blokk/utils/ByteFlags.kt
Normal file
9
blokk-api/src/main/kotlin/space/blokk/utils/ByteFlags.kt
Normal file
|
@ -0,0 +1,9 @@
|
|||
package space.blokk.utils
|
||||
|
||||
import kotlin.experimental.and
|
||||
|
||||
fun Byte.checkBit(bitIndex: Int): Boolean {
|
||||
val flag = (0x1 shl bitIndex).toByte()
|
||||
|
||||
return (this and flag) == flag
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package space.blokk.worlds
|
||||
|
||||
enum class WorldDifficulty {
|
||||
PEACEFUL,
|
||||
EASY,
|
||||
NORMAL,
|
||||
HARD
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package space.blokk.worlds
|
||||
|
||||
enum class WorldDimension {
|
||||
OVERWORLD,
|
||||
NETHER,
|
||||
END
|
||||
}
|
10
blokk-api/src/main/kotlin/space/blokk/worlds/WorldType.kt
Normal file
10
blokk-api/src/main/kotlin/space/blokk/worlds/WorldType.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package space.blokk.worlds
|
||||
|
||||
enum class WorldType {
|
||||
DEFAULT,
|
||||
FLAT,
|
||||
LARGE_BIOMES,
|
||||
AMPLIFIED,
|
||||
CUSTOMIZED,
|
||||
BUFFET
|
||||
}
|
|
@ -81,7 +81,7 @@ class BlokkServer internal constructor() : Server {
|
|||
}
|
||||
|
||||
fun start() {
|
||||
logger info "Starting BlokkServer (${if (VERSION == "development") VERSION else "v$VERSION"})"
|
||||
logger info "Starting BlokkServer ($VERSION_WITH_V)"
|
||||
logger trace "Configuration: $config"
|
||||
|
||||
socketServer.bind()
|
||||
|
@ -91,6 +91,7 @@ class BlokkServer internal constructor() : Server {
|
|||
companion object {
|
||||
lateinit var instance: BlokkServer; private set
|
||||
val VERSION = BlokkServer::class.java.`package`.implementationVersion ?: "development"
|
||||
val VERSION_WITH_V = if (VERSION == "development") VERSION else "v$VERSION"
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package space.blokk.net
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.channel.Channel
|
||||
import kotlinx.coroutines.*
|
||||
import space.blokk.BlokkServer
|
||||
|
@ -14,6 +15,8 @@ import space.blokk.net.protocols.Protocol
|
|||
import space.blokk.net.protocols.handshaking.HandshakingProtocol
|
||||
import space.blokk.net.protocols.login.DisconnectPacket
|
||||
import space.blokk.net.protocols.login.LoginProtocol
|
||||
import space.blokk.net.protocols.play.OutgoingPluginMessagePacket
|
||||
import space.blokk.net.protocols.play.PlayProtocol
|
||||
import space.blokk.server.events.SessionInitializedEvent
|
||||
import space.blokk.utils.awaitSuspending
|
||||
import java.net.InetAddress
|
||||
|
@ -35,11 +38,14 @@ class BlokkSession(private val channel: Channel) : Session {
|
|||
|
||||
override val scope = CoroutineScope(Dispatchers.Unconfined + CoroutineName(identifier))
|
||||
override val eventBus = EventBus(SessionEvent::class, scope)
|
||||
override var brand: String? = null; internal set
|
||||
|
||||
var loginState: LoginState = LoginState.NotStarted
|
||||
private var active: Boolean = true
|
||||
private var disconnectReason: String? = null
|
||||
|
||||
val joinProcedure = JoinProcedure(this)
|
||||
|
||||
init {
|
||||
eventBus.register(object : Listener {
|
||||
@EventHandler(priority = EventPriority.INTERNAL)
|
||||
|
@ -108,13 +114,20 @@ class BlokkSession(private val channel: Channel) : Session {
|
|||
}
|
||||
}
|
||||
|
||||
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))
|
||||
data.release()
|
||||
}
|
||||
|
||||
sealed class LoginState(val name: String) {
|
||||
object NotStarted: LoginState("NOT_STARTED")
|
||||
object NotStarted : LoginState("NOT_STARTED")
|
||||
|
||||
class WaitingForVerification(val username: String, val verifyToken: ByteArray): LoginState("WAITING_FOR_VERIFICATION")
|
||||
class WaitingForVerification(val username: String, val verifyToken: ByteArray) :
|
||||
LoginState("WAITING_FOR_VERIFICATION")
|
||||
|
||||
class Encrypted(val username: String): LoginState("ENCRYPTED")
|
||||
class Encrypted(val username: String) : LoginState("ENCRYPTED")
|
||||
|
||||
class Success(val username: String, val uuid: UUID): LoginState("SUCCESS")
|
||||
class Success(val username: String, val uuid: UUID) : LoginState("SUCCESS")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import space.blokk.net.protocols.handshaking.HandshakingProtocol
|
|||
import space.blokk.net.protocols.handshaking.HandshakingProtocolHandler
|
||||
import space.blokk.net.protocols.login.LoginProtocol
|
||||
import space.blokk.net.protocols.login.LoginProtocolHandler
|
||||
import space.blokk.net.protocols.play.PlayProtocol
|
||||
import space.blokk.net.protocols.play.PlayProtocolHandler
|
||||
import space.blokk.net.protocols.status.StatusProtocol
|
||||
import space.blokk.net.protocols.status.StatusProtocolHandler
|
||||
|
||||
|
@ -36,6 +38,7 @@ object SessionPacketReceivedEventHandler {
|
|||
HandshakingProtocol -> HandshakingProtocolHandler
|
||||
StatusProtocol -> StatusProtocolHandler
|
||||
LoginProtocol -> LoginProtocolHandler
|
||||
PlayProtocol -> PlayProtocolHandler
|
||||
else -> return
|
||||
}
|
||||
|
||||
|
|
131
blokk-server/src/main/kotlin/space/blokk/net/JoinProcedure.kt
Normal file
131
blokk-server/src/main/kotlin/space/blokk/net/JoinProcedure.kt
Normal file
|
@ -0,0 +1,131 @@
|
|||
package space.blokk.net
|
||||
|
||||
import io.netty.buffer.Unpooled
|
||||
import space.blokk.BlokkServer
|
||||
import space.blokk.GameMode
|
||||
import space.blokk.chat.TextComponent
|
||||
import space.blokk.net.MinecraftDataTypes.writeString
|
||||
import space.blokk.net.protocols.login.EncryptionRequestPacket
|
||||
import space.blokk.net.protocols.login.EncryptionResponsePacket
|
||||
import space.blokk.net.protocols.login.LoginStartPacket
|
||||
import space.blokk.net.protocols.login.LoginSuccessPacket
|
||||
import space.blokk.net.protocols.play.JoinGamePacket
|
||||
import space.blokk.net.protocols.play.PlayProtocol
|
||||
import space.blokk.net.protocols.play.PlayerAbilitiesPacket
|
||||
import space.blokk.net.protocols.play.ServerDifficultyPacket
|
||||
import space.blokk.utils.AuthenticationHelper
|
||||
import space.blokk.utils.EncryptionUtils
|
||||
import space.blokk.worlds.WorldDifficulty
|
||||
import space.blokk.worlds.WorldDimension
|
||||
import space.blokk.worlds.WorldType
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
class JoinProcedure(val session: BlokkSession) {
|
||||
suspend fun start(packet: LoginStartPacket) {
|
||||
if (session.loginState !is BlokkSession.LoginState.NotStarted)
|
||||
throw IllegalStateException("loginState is not NotStarted")
|
||||
|
||||
if (BlokkServer.instance.config.authenticateAndEncrypt) {
|
||||
val verifyToken = EncryptionUtils.generateVerifyToken()
|
||||
session.loginState = BlokkSession.LoginState.WaitingForVerification(packet.username, verifyToken)
|
||||
|
||||
session.send(EncryptionRequestPacket(BlokkServer.instance.x509EncodedPublicKey, verifyToken))
|
||||
} else {
|
||||
session.send(
|
||||
LoginSuccessPacket(
|
||||
UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray()),
|
||||
packet.username
|
||||
)
|
||||
)
|
||||
|
||||
afterLogin()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onEncryptionResponse(packet: EncryptionResponsePacket) {
|
||||
var loginState = session.loginState
|
||||
if (loginState !is BlokkSession.LoginState.WaitingForVerification)
|
||||
throw IllegalStateException("loginState is not WaitingForVerification")
|
||||
|
||||
val cipher = Cipher.getInstance("RSA")
|
||||
cipher.init(Cipher.DECRYPT_MODE, BlokkServer.instance.keyPair.private)
|
||||
|
||||
// Decrypt verify token
|
||||
val verifyToken: ByteArray = cipher.doFinal(packet.verifyToken)
|
||||
|
||||
if (!verifyToken.contentEquals((session.loginState as BlokkSession.LoginState.WaitingForVerification).verifyToken)) {
|
||||
session.disconnect(TextComponent of "Encryption failed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt shared secret
|
||||
val sharedSecretKey = SecretKeySpec(cipher.doFinal(packet.sharedSecret), "AES")
|
||||
|
||||
loginState = BlokkSession.LoginState.Encrypted(loginState.username)
|
||||
session.loginState = loginState
|
||||
session.enableEncryption(sharedSecretKey)
|
||||
|
||||
val hash = MessageDigest.getInstance("SHA-1")
|
||||
hash.update(sharedSecretKey.encoded)
|
||||
hash.update(BlokkServer.instance.x509EncodedPublicKey)
|
||||
val hashString = EncryptionUtils.toMinecraftStyleSha1String(hash.digest())
|
||||
|
||||
val result = AuthenticationHelper.authenticate(hashString, loginState.username)
|
||||
loginState = BlokkSession.LoginState.Success(result.username, result.uuid)
|
||||
session.loginState = loginState
|
||||
|
||||
session.send(LoginSuccessPacket(result.uuid, result.username))
|
||||
|
||||
afterLogin()
|
||||
}
|
||||
|
||||
private suspend fun afterLogin() {
|
||||
session.currentProtocol = PlayProtocol
|
||||
|
||||
// TODO: Use real data
|
||||
session.send(
|
||||
JoinGamePacket(
|
||||
0,
|
||||
GameMode.CREATIVE,
|
||||
false,
|
||||
WorldDimension.OVERWORLD,
|
||||
12,
|
||||
WorldType.DEFAULT,
|
||||
32,
|
||||
reducedDebugInfo = false,
|
||||
respawnScreenEnabled = true
|
||||
)
|
||||
)
|
||||
|
||||
session.sendPluginMessage(
|
||||
"minecraft:brand",
|
||||
Unpooled.buffer().writeString("Blokk ${BlokkServer.VERSION_WITH_V}")
|
||||
)
|
||||
|
||||
// TODO: Use real data
|
||||
session.send(
|
||||
PlayerAbilitiesPacket(
|
||||
invulnerable = false,
|
||||
flying = false,
|
||||
flyingAllowed = true,
|
||||
instantlyBreakBlocks = true,
|
||||
flyingSpeed = 0.2f,
|
||||
fieldOfView = 0.2f
|
||||
)
|
||||
)
|
||||
|
||||
joinWorld()
|
||||
}
|
||||
|
||||
private suspend fun joinWorld() {
|
||||
session.send(
|
||||
ServerDifficultyPacket(
|
||||
WorldDifficulty.NORMAL,
|
||||
true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,52 +1,14 @@
|
|||
package space.blokk.net.protocols.login
|
||||
|
||||
import space.blokk.BlokkServer
|
||||
import space.blokk.chat.TextComponent
|
||||
import space.blokk.net.BlokkSession
|
||||
import space.blokk.net.PacketReceivedEventHandler
|
||||
import space.blokk.net.protocols.play.PlayProtocol
|
||||
import space.blokk.utils.AuthenticationHelper
|
||||
import space.blokk.utils.toMinecraftStyleSha1String
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object EncryptionResponsePacketHandler : PacketReceivedEventHandler<EncryptionResponsePacket>() {
|
||||
override suspend fun handle(session: BlokkSession, packet: EncryptionResponsePacket) {
|
||||
var loginState = session.loginState
|
||||
if (loginState !is BlokkSession.LoginState.WaitingForVerification) {
|
||||
try {
|
||||
session.joinProcedure.onEncryptionResponse(packet)
|
||||
} catch (e: IllegalStateException) {
|
||||
session.failBecauseOfClient("Client sent EncryptionResponsePacket although loginState is ${session.loginState.name}")
|
||||
return
|
||||
}
|
||||
|
||||
val cipher = Cipher.getInstance("RSA")
|
||||
cipher.init(Cipher.DECRYPT_MODE, BlokkServer.instance.keyPair.private)
|
||||
|
||||
// Decrypt verify token
|
||||
val verifyToken: ByteArray = cipher.doFinal(packet.verifyToken)
|
||||
|
||||
if (!verifyToken.contentEquals(loginState.verifyToken)) {
|
||||
session.disconnect(TextComponent of "Encryption failed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt shared secret
|
||||
val sharedSecretKey = SecretKeySpec(cipher.doFinal(packet.sharedSecret), "AES")
|
||||
|
||||
loginState = BlokkSession.LoginState.Encrypted(loginState.username)
|
||||
session.loginState = loginState
|
||||
session.enableEncryption(sharedSecretKey)
|
||||
|
||||
val hash = MessageDigest.getInstance("SHA-1")
|
||||
hash.update(sharedSecretKey.encoded)
|
||||
hash.update(BlokkServer.instance.x509EncodedPublicKey)
|
||||
val hashString = toMinecraftStyleSha1String(hash.digest())
|
||||
|
||||
val result = AuthenticationHelper.authenticate(hashString, loginState.username)
|
||||
loginState = BlokkSession.LoginState.Success(result.username, result.uuid)
|
||||
session.loginState = loginState
|
||||
|
||||
session.send(LoginSuccessPacket(result.uuid, result.username))
|
||||
session.currentProtocol = PlayProtocol
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,14 @@
|
|||
package space.blokk.net.protocols.login
|
||||
|
||||
import space.blokk.BlokkServer
|
||||
import space.blokk.net.BlokkSession
|
||||
import space.blokk.net.PacketReceivedEventHandler
|
||||
import space.blokk.net.protocols.play.PlayProtocol
|
||||
import space.blokk.utils.EncryptionUtils
|
||||
import java.util.*
|
||||
|
||||
object LoginStartPacketHandler : PacketReceivedEventHandler<LoginStartPacket>() {
|
||||
override suspend fun handle(session: BlokkSession, packet: LoginStartPacket) {
|
||||
if (session.loginState != BlokkSession.LoginState.NotStarted) {
|
||||
try {
|
||||
session.joinProcedure.start(packet)
|
||||
} catch (e: IllegalStateException) {
|
||||
session.failBecauseOfClient("Client sent LoginStartPacket although loginState is ${session.loginState.name}")
|
||||
return
|
||||
}
|
||||
|
||||
if (BlokkServer.instance.config.authenticateAndEncrypt) {
|
||||
val verifyToken = EncryptionUtils.generateVerifyToken()
|
||||
session.loginState = BlokkSession.LoginState.WaitingForVerification(packet.username, verifyToken)
|
||||
|
||||
session.send(EncryptionRequestPacket(BlokkServer.instance.x509EncodedPublicKey, verifyToken))
|
||||
} else {
|
||||
session.send(
|
||||
LoginSuccessPacket(
|
||||
UUID.nameUUIDFromBytes("OfflinePlayer:${packet.username}".toByteArray()),
|
||||
packet.username
|
||||
)
|
||||
)
|
||||
|
||||
session.currentProtocol = PlayProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import space.blokk.net.BlokkSession
|
||||
import space.blokk.net.PacketReceivedEventHandler
|
||||
|
||||
object ClientSettingsPacketHandler : PacketReceivedEventHandler<ClientSettingsPacket>() {
|
||||
override suspend fun handle(session: BlokkSession, packet: ClientSettingsPacket) {
|
||||
// TODO: Save the settings
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.Unpooled
|
||||
import space.blokk.net.BlokkSession
|
||||
import space.blokk.net.MinecraftDataTypes.readString
|
||||
import space.blokk.net.PacketReceivedEventHandler
|
||||
import space.blokk.net.events.ClientBrandReceivedEvent
|
||||
|
||||
object IncomingPluginMessagePacketHandler : PacketReceivedEventHandler<IncomingPluginMessagePacket>() {
|
||||
override suspend fun handle(session: BlokkSession, packet: IncomingPluginMessagePacket) {
|
||||
if (packet.channel == "minecraft:brand") {
|
||||
if (session.brand != null) session.failBecauseOfClient("The client send his brand string more than once.")
|
||||
else {
|
||||
val buffer: ByteBuf = Unpooled.copiedBuffer(packet.data)
|
||||
val brand = buffer.readString()
|
||||
buffer.release()
|
||||
|
||||
session.brand = brand
|
||||
session.emit(ClientBrandReceivedEvent(session, brand))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package space.blokk.net.protocols.play
|
||||
|
||||
import space.blokk.net.ProtocolPacketReceivedEventHandler
|
||||
|
||||
// NOTE: PacketReceivedEventHandler.of<T> MUST have T specified correctly, otherwise the code breaks at runtime
|
||||
object PlayProtocolHandler : ProtocolPacketReceivedEventHandler(
|
||||
mapOf(
|
||||
ClientSettingsPacket to ClientSettingsPacketHandler,
|
||||
IncomingPluginMessagePacket to IncomingPluginMessagePacketHandler
|
||||
)
|
||||
)
|
|
@ -1,10 +1,10 @@
|
|||
package space.blokk.utils
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
object EncryptionUtils {
|
||||
fun generateKeyPair(): KeyPair {
|
||||
try {
|
||||
|
@ -25,4 +25,6 @@ object EncryptionUtils {
|
|||
}
|
||||
|
||||
fun generateVerifyToken() = ByteArray(4).also { Random.Default.nextBytes(it) }
|
||||
|
||||
fun toMinecraftStyleSha1String(value: ByteArray): String = BigInteger(value).toString(16)
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package space.blokk.utils
|
||||
|
||||
import java.math.BigInteger
|
||||
|
||||
fun toMinecraftStyleSha1String(value: ByteArray): String = BigInteger(value).toString(16)
|
Reference in a new issue