diff --git a/.gitignore b/.gitignore index e996519..c4384f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Project exclude paths /.gradle/ +/build/ /blokk-api/build/ /blokk-server/build/ /data diff --git a/blokk-api/src/main/kotlin/space/blokk/GameMode.kt b/blokk-api/src/main/kotlin/space/blokk/GameMode.kt new file mode 100644 index 0000000..a7da7e5 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/GameMode.kt @@ -0,0 +1,8 @@ +package space.blokk + +enum class GameMode { + SURVIVAL, + ADVENTURE, + CREATIVE, + SPECTATOR +} diff --git a/blokk-api/src/main/kotlin/space/blokk/events/EventTarget.kt b/blokk-api/src/main/kotlin/space/blokk/events/EventTarget.kt index 3874609..1d65846 100644 --- a/blokk-api/src/main/kotlin/space/blokk/events/EventTarget.kt +++ b/blokk-api/src/main/kotlin/space/blokk/events/EventTarget.kt @@ -2,4 +2,24 @@ package space.blokk.events interface EventTarget { val eventBus: EventBus + + /** + * 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) } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/MinecraftDataTypes.kt b/blokk-api/src/main/kotlin/space/blokk/net/MinecraftDataTypes.kt index 0e99f78..325db04 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/MinecraftDataTypes.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/MinecraftDataTypes.kt @@ -60,7 +60,7 @@ object MinecraftDataTypes { * * @see https://wiki.vg/Protocol#VarInt_and_VarLong */ - 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 https://wiki.vg/Protocol#Data_types */ - 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 } } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/PacketDecodingException.kt b/blokk-api/src/main/kotlin/space/blokk/net/PacketDecodingException.kt new file mode 100644 index 0000000..123a029 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/PacketDecodingException.kt @@ -0,0 +1,3 @@ +package space.blokk.net + +class PacketDecodingException : Exception("Packet decoding failed") diff --git a/blokk-api/src/main/kotlin/space/blokk/net/Session.kt b/blokk-api/src/main/kotlin/space/blokk/net/Session.kt index 6ac5a86..4e39a7f 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/Session.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/Session.kt @@ -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 { /** * 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? } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/events/ClientBrandReceivedEvent.kt b/blokk-api/src/main/kotlin/space/blokk/net/events/ClientBrandReceivedEvent.kt new file mode 100644 index 0000000..c83edd8 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/events/ClientBrandReceivedEvent.kt @@ -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) diff --git a/blokk-api/src/main/kotlin/space/blokk/net/events/PacketReceivedEvent.kt b/blokk-api/src/main/kotlin/space/blokk/net/events/PacketReceivedEvent.kt index 1fbbabe..94eb150 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/events/PacketReceivedEvent.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/events/PacketReceivedEvent.kt @@ -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(session: Session, var packet: T) : SessionEvent(session), Cancellable { override var isCancelled = false } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/events/PacketSendEvent.kt b/blokk-api/src/main/kotlin/space/blokk/net/events/PacketSendEvent.kt index 852b325..19a7420 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/events/PacketSendEvent.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/events/PacketSendEvent.kt @@ -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 } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/events/ServerListInfoRequestEvent.kt b/blokk-api/src/main/kotlin/space/blokk/net/events/ServerListInfoRequestEvent.kt index 7f13a77..21ece6f 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/events/ServerListInfoRequestEvent.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/events/ServerListInfoRequestEvent.kt @@ -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 diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/Protocol.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/Protocol.kt index cf0415d..8ef8619 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/protocols/Protocol.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/Protocol.kt @@ -1,6 +1,6 @@ package space.blokk.net.protocols -abstract class Protocol internal constructor(val name: String, packets: Set>) { +abstract class Protocol internal constructor(val name: String, vararg packets: PacketCompanion<*>) { val incomingPackets = packets.filterIsInstance>() val incomingPacketsByID = incomingPackets.mapToIDMap() val outgoingPackets = packets.filterIsInstance>() diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/handshaking/HandshakingProtocol.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/handshaking/HandshakingProtocol.kt index a014003..d8a61c2 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/protocols/handshaking/HandshakingProtocol.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/handshaking/HandshakingProtocol.kt @@ -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) diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginPluginRequestPacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginPluginRequestPacket.kt index 82d9379..d94f9fb 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginPluginRequestPacket.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginPluginRequestPacket.kt @@ -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(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 } } diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginProtocol.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginProtocol.kt index 75d2bf9..80360f2 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginProtocol.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/login/LoginProtocol.kt @@ -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 ) diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/ClientSettingsPacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/ClientSettingsPacket.kt new file mode 100644 index 0000000..5e0fb78 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/ClientSettingsPacket.kt @@ -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(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 + ) + } + } + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/IncomingPluginMessagePacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/IncomingPluginMessagePacket.kt new file mode 100644 index 0000000..1d6643b --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/IncomingPluginMessagePacket.kt @@ -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(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 + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/JoinGamePacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/JoinGamePacket.kt new file mode 100644 index 0000000..8eb8e87 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/JoinGamePacket.kt @@ -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(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) + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/OutgoingPluginMessagePacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/OutgoingPluginMessagePacket.kt new file mode 100644 index 0000000..29d82bf --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/OutgoingPluginMessagePacket.kt @@ -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(0x19, OutgoingPluginMessagePacket::class) + + override fun encode(dst: ByteBuf) { + dst.writeString(channel) + dst.writeBytes(data) + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocol.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocol.kt index ba5ea91..2616742 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocol.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocol.kt @@ -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 +) diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayerAbilitiesPacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayerAbilitiesPacket.kt new file mode 100644 index 0000000..09b9141 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/PlayerAbilitiesPacket.kt @@ -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(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) + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/ServerDifficultyPacket.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/ServerDifficultyPacket.kt new file mode 100644 index 0000000..e5c9704 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/play/ServerDifficultyPacket.kt @@ -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(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) + } +} diff --git a/blokk-api/src/main/kotlin/space/blokk/net/protocols/status/StatusProtocol.kt b/blokk-api/src/main/kotlin/space/blokk/net/protocols/status/StatusProtocol.kt index a97e036..195f998 100644 --- a/blokk-api/src/main/kotlin/space/blokk/net/protocols/status/StatusProtocol.kt +++ b/blokk-api/src/main/kotlin/space/blokk/net/protocols/status/StatusProtocol.kt @@ -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 +) diff --git a/blokk-api/src/main/kotlin/space/blokk/players/ChatMode.kt b/blokk-api/src/main/kotlin/space/blokk/players/ChatMode.kt new file mode 100644 index 0000000..5a735d3 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/players/ChatMode.kt @@ -0,0 +1,7 @@ +package space.blokk.players + +enum class ChatMode { + ENABLED, + COMMANDS_ONLY, + HIDDEN +} diff --git a/blokk-api/src/main/kotlin/space/blokk/players/DisplayedSkinParts.kt b/blokk-api/src/main/kotlin/space/blokk/players/DisplayedSkinParts.kt new file mode 100644 index 0000000..6c4eaa0 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/players/DisplayedSkinParts.kt @@ -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 +) diff --git a/blokk-api/src/main/kotlin/space/blokk/players/MainHand.kt b/blokk-api/src/main/kotlin/space/blokk/players/MainHand.kt new file mode 100644 index 0000000..6dc35e2 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/players/MainHand.kt @@ -0,0 +1,6 @@ +package space.blokk.players + +enum class MainHand { + LEFT, + RIGHT +} diff --git a/blokk-api/src/main/kotlin/space/blokk/utils/ByteFlags.kt b/blokk-api/src/main/kotlin/space/blokk/utils/ByteFlags.kt new file mode 100644 index 0000000..bf7d722 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/utils/ByteFlags.kt @@ -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 +} diff --git a/blokk-api/src/main/kotlin/space/blokk/worlds/WorldDifficulty.kt b/blokk-api/src/main/kotlin/space/blokk/worlds/WorldDifficulty.kt new file mode 100644 index 0000000..475bee2 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/worlds/WorldDifficulty.kt @@ -0,0 +1,8 @@ +package space.blokk.worlds + +enum class WorldDifficulty { + PEACEFUL, + EASY, + NORMAL, + HARD +} diff --git a/blokk-api/src/main/kotlin/space/blokk/worlds/WorldDimension.kt b/blokk-api/src/main/kotlin/space/blokk/worlds/WorldDimension.kt new file mode 100644 index 0000000..66c5f15 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/worlds/WorldDimension.kt @@ -0,0 +1,7 @@ +package space.blokk.worlds + +enum class WorldDimension { + OVERWORLD, + NETHER, + END +} diff --git a/blokk-api/src/main/kotlin/space/blokk/worlds/WorldType.kt b/blokk-api/src/main/kotlin/space/blokk/worlds/WorldType.kt new file mode 100644 index 0000000..acc5bd3 --- /dev/null +++ b/blokk-api/src/main/kotlin/space/blokk/worlds/WorldType.kt @@ -0,0 +1,10 @@ +package space.blokk.worlds + +enum class WorldType { + DEFAULT, + FLAT, + LARGE_BIOMES, + AMPLIFIED, + CUSTOMIZED, + BUFFET +} diff --git a/blokk-server/src/main/kotlin/space/blokk/BlokkServer.kt b/blokk-server/src/main/kotlin/space/blokk/BlokkServer.kt index 93a25ad..c38cec6 100644 --- a/blokk-server/src/main/kotlin/space/blokk/BlokkServer.kt +++ b/blokk-server/src/main/kotlin/space/blokk/BlokkServer.kt @@ -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) { diff --git a/blokk-server/src/main/kotlin/space/blokk/net/BlokkSession.kt b/blokk-server/src/main/kotlin/space/blokk/net/BlokkSession.kt index 7717ea3..652c0d5 100644 --- a/blokk-server/src/main/kotlin/space/blokk/net/BlokkSession.kt +++ b/blokk-server/src/main/kotlin/space/blokk/net/BlokkSession.kt @@ -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") } } diff --git a/blokk-server/src/main/kotlin/space/blokk/net/DefaultPacketReceivedEventHandler.kt b/blokk-server/src/main/kotlin/space/blokk/net/DefaultPacketReceivedEventHandler.kt index c1c5351..8f55848 100644 --- a/blokk-server/src/main/kotlin/space/blokk/net/DefaultPacketReceivedEventHandler.kt +++ b/blokk-server/src/main/kotlin/space/blokk/net/DefaultPacketReceivedEventHandler.kt @@ -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 } diff --git a/blokk-server/src/main/kotlin/space/blokk/net/JoinProcedure.kt b/blokk-server/src/main/kotlin/space/blokk/net/JoinProcedure.kt new file mode 100644 index 0000000..b6cece5 --- /dev/null +++ b/blokk-server/src/main/kotlin/space/blokk/net/JoinProcedure.kt @@ -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 + ) + ) + } +} diff --git a/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/EncryptionResponsePacketHandler.kt b/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/EncryptionResponsePacketHandler.kt index 112332e..3b6e334 100644 --- a/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/EncryptionResponsePacketHandler.kt +++ b/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/EncryptionResponsePacketHandler.kt @@ -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() { 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 } } diff --git a/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/LoginStartPacketHandler.kt b/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/LoginStartPacketHandler.kt index 88c17f0..5de6547 100644 --- a/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/LoginStartPacketHandler.kt +++ b/blokk-server/src/main/kotlin/space/blokk/net/protocols/login/LoginStartPacketHandler.kt @@ -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() { 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 } } } diff --git a/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/ClientSettingsPacketHandler.kt b/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/ClientSettingsPacketHandler.kt new file mode 100644 index 0000000..ad40662 --- /dev/null +++ b/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/ClientSettingsPacketHandler.kt @@ -0,0 +1,10 @@ +package space.blokk.net.protocols.play + +import space.blokk.net.BlokkSession +import space.blokk.net.PacketReceivedEventHandler + +object ClientSettingsPacketHandler : PacketReceivedEventHandler() { + override suspend fun handle(session: BlokkSession, packet: ClientSettingsPacket) { + // TODO: Save the settings + } +} diff --git a/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/IncomingPluginMessagePacketHandler.kt b/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/IncomingPluginMessagePacketHandler.kt new file mode 100644 index 0000000..d24b57a --- /dev/null +++ b/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/IncomingPluginMessagePacketHandler.kt @@ -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() { + 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)) + } + } + } +} diff --git a/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocolHandler.kt b/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocolHandler.kt new file mode 100644 index 0000000..e084df7 --- /dev/null +++ b/blokk-server/src/main/kotlin/space/blokk/net/protocols/play/PlayProtocolHandler.kt @@ -0,0 +1,11 @@ +package space.blokk.net.protocols.play + +import space.blokk.net.ProtocolPacketReceivedEventHandler + +// NOTE: PacketReceivedEventHandler.of MUST have T specified correctly, otherwise the code breaks at runtime +object PlayProtocolHandler : ProtocolPacketReceivedEventHandler( + mapOf( + ClientSettingsPacket to ClientSettingsPacketHandler, + IncomingPluginMessagePacket to IncomingPluginMessagePacketHandler + ) +) diff --git a/blokk-server/src/main/kotlin/space/blokk/utils/EncryptionUtils.kt b/blokk-server/src/main/kotlin/space/blokk/utils/EncryptionUtils.kt index b974c50..bb5b10e 100644 --- a/blokk-server/src/main/kotlin/space/blokk/utils/EncryptionUtils.kt +++ b/blokk-server/src/main/kotlin/space/blokk/utils/EncryptionUtils.kt @@ -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) } diff --git a/blokk-server/src/main/kotlin/space/blokk/utils/MinecraftSha1.kt b/blokk-server/src/main/kotlin/space/blokk/utils/MinecraftSha1.kt deleted file mode 100644 index f92bddb..0000000 --- a/blokk-server/src/main/kotlin/space/blokk/utils/MinecraftSha1.kt +++ /dev/null @@ -1,5 +0,0 @@ -package space.blokk.utils - -import java.math.BigInteger - -fun toMinecraftStyleSha1String(value: ByteArray): String = BigInteger(value).toString(16)