Archived
1
0
Fork 0

Add documentation, move ByteBuf extensions into an object and generify SessionContainer

This commit is contained in:
Moritz Ruth 2020-08-13 16:53:02 +02:00
parent 3c85b6c105
commit 6daa459fa6
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
23 changed files with 215 additions and 132 deletions

View file

@ -1,14 +1,20 @@
package space.blokk package space.blokk
import space.blokk.net.SessionContainer import space.blokk.events.EventTargetGroup
import space.blokk.net.Session
import space.blokk.server.Server import space.blokk.server.Server
interface BlokkProvider { interface BlokkProvider {
val server: Server val server: Server
val sessions: SessionContainer
/**
* [EventTargetGroup] instance containing all sessions connected to the server.
*/
val sessions: EventTargetGroup<Session>
} }
object Blokk: BlokkProvider { object Blokk: BlokkProvider {
// Is assigned by BlokkServer using reflection
private var provider: BlokkProvider? = null private var provider: BlokkProvider? = null
override val server get() = provider!!.server override val server get() = provider!!.server
override val sessions get() = provider!!.sessions override val sessions get() = provider!!.sessions

View file

@ -19,6 +19,9 @@ sealed class ChatComponent {
data class TextComponent(val text: String, override val extra: ChatComponent? = null): ChatComponent() { data class TextComponent(val text: String, override val extra: ChatComponent? = null): ChatComponent() {
companion object { companion object {
/**
* Creates a new [TextComponent] instance using [text] and returns it.
*/
infix fun of(text: String) = TextComponent(text) infix fun of(text: String) = TextComponent(text)
} }
} }

View file

@ -1,9 +1,9 @@
package space.blokk.chat package space.blokk.chat
/** /**
* Legacy formatting codes. You should use [space.blokk.chat.ChatComponent] whenever it it possible, but sometimes * Legacy formatting codes. You should use [ChatComponent][space.blokk.chat.ChatComponent] whenever it's possible, but sometimes
* these codes are required, for example in the name of a player in the server list sample * these codes are required, for example in
* ([space.blokk.net.protocols.status.ResponsePacket.Players.SampleEntry]). * [the name of a player in a server list sample][space.blokk.net.protocols.status.ResponsePacket.Players.SampleEntry.name].
*/ */
enum class FormattingCode(private val char: Char) { enum class FormattingCode(private val char: Char) {
BLACK('0'), BLACK('0'),

View file

@ -3,3 +3,13 @@ package space.blokk.events
interface Cancellable { interface Cancellable {
var isCancelled: Boolean var isCancelled: Boolean
} }
/**
* Only executes [fn] if [isCancelled][Cancellable.isCancelled] is true.
*/
inline fun Cancellable.ifCancelled(fn: () -> Unit) = if (isCancelled) fn() else Unit
/**
* Only executes [fn] if [isCancelled][Cancellable.isCancelled] is false.
*/
inline fun Cancellable.ifNotCancelled(fn: () -> Unit) = if (!isCancelled) fn() else Unit

View file

@ -8,15 +8,24 @@ class EventBus<EventT: Event>(eventType: KClass<EventT>) {
private val eventType: Class<EventT> = eventType.java private val eventType: Class<EventT> = eventType.java
/** /**
* All event handlers, sorted by their priority and the order in which they were inserted * All event handlers, sorted by their priority and the order in which they were registered.
*/ */
private val handlers = mutableListOf<Handler>() private val handlers = mutableListOf<Handler>()
/**
* Invokes all previously registered event handlers sorted by their priority
* and the order in which they were registered.
*/
fun <T: EventT> emit(event: T): T { fun <T: EventT> emit(event: T): T {
handlers.filter { it.eventType.isInstance(event) }.forEach { it.fn.invoke(it.listener, event) } handlers.filter { it.eventType.isInstance(event) }.forEach { it.fn.invoke(it.listener, event) }
return event return event
} }
/**
* Registers all [event handlers][EventHandler] in [listener] to be invoked when their corresponding event is emitted.
*
* @throws InvalidEventHandlerException if one of the event handlers does not meet the requirements
*/
fun register(listener: Listener) { fun register(listener: Listener) {
val handlersOfListener = listener::class.java.methods val handlersOfListener = listener::class.java.methods
.mapNotNull { method -> method.getAnnotation(EventHandler::class.java)?.let { method to it } } .mapNotNull { method -> method.getAnnotation(EventHandler::class.java)?.let { method to it } }
@ -39,7 +48,7 @@ class EventBus<EventT: Event>(eventType: KClass<EventT>) {
} }
} }
class InvalidEventHandlerException(message: String): Exception(message) class InvalidEventHandlerException internal constructor(message: String): Exception(message)
private data class Handler( private data class Handler(
val eventType: Class<out Event>, val eventType: Class<out Event>,

View file

@ -0,0 +1,5 @@
package space.blokk.events
interface EventTarget<T: Event> {
val eventBus: EventBus<T>
}

View file

@ -0,0 +1,5 @@
package space.blokk.events
interface EventTargetGroup<T: EventTarget<*>> : Iterable<T> {
fun registerListener(listener: Listener)
}

View file

@ -1,70 +0,0 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import java.io.IOException
import kotlin.experimental.and
fun ByteBuf.readVarInt(): Int {
var bytesRead = 0
var result = 0
do {
if (bytesRead == 5) throw IOException("VarInt is longer than the maximum of 5 bytes")
val byte = readByte()
val value: Int = (byte and 0b01111111).toInt()
result = result or (value shl (7 * bytesRead))
bytesRead += 1
} while ((byte and 0b10000000.toByte()).toInt() != 0)
return result
}
fun ByteBuf.varIntReadable(): Boolean {
if (readableBytes() > 5) {
// maximum VarInt size
return true
}
val initialIndex = readerIndex()
do {
if (readableBytes() < 1) {
readerIndex(initialIndex)
return false;
}
val value = readByte().toInt()
} while ((value and 0b10000000) != 0);
readerIndex(initialIndex)
return true;
}
fun ByteBuf.writeVarInt(value: Int) {
var v = value
var part: Byte
while (true) {
part = (v and 0x7F).toByte()
v = v ushr 7
if (v != 0) {
part = (part.toInt() or 0x80).toByte()
}
writeByte(part.toInt())
if (v == 0) {
break
}
}
}
fun ByteBuf.readString(): String {
val length = readVarInt()
val bytes = ByteArray(length)
readBytes(bytes)
return bytes.toString(Charsets.UTF_8)
}
fun ByteBuf.writeString(value: String) {
val bytes = value.toByteArray(Charsets.UTF_8)
writeVarInt(bytes.size)
writeBytes(bytes)
}

View file

@ -0,0 +1,103 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import java.io.IOException
import kotlin.experimental.and
/**
* Extension functions on [ByteBuf] for reading and writing data types used by the Minecraft protocol
*
* You can access them using the [with] function (`with(MinecraftDataTypes) { ... }`).
*/
object MinecraftDataTypes {
/**
* Gets a variable length integer at the current readerIndex and increases the readerIndex by up to 5 in this buffer.
*
* @throws IOException if the VarInt is longer than the maximum of 5 bytes
* @see <a href="https://wiki.vg/Protocol#VarInt_and_VarLong">https://wiki.vg/Protocol#VarInt_and_VarLong</a>
*/
fun ByteBuf.readVarInt(): Int {
var bytesRead = 0
var result = 0
do {
if (bytesRead == 5) throw IOException("VarInt is longer than the maximum of 5 bytes")
val byte = readByte()
val value: Int = (byte and 0b01111111).toInt()
result = result or (value shl (7 * bytesRead))
bytesRead += 1
} while ((byte and 0b10000000.toByte()).toInt() != 0)
return result
}
/**
* Checks if a VarInt can be read starting from the current readerIndex.
*/
fun ByteBuf.varIntReadable(): Boolean {
if (readableBytes() > 5) {
// maximum VarInt size
return true
}
val initialIndex = readerIndex()
do {
if (readableBytes() < 1) {
readerIndex(initialIndex)
return false
}
val value = readByte().toInt()
} while ((value and 0b10000000) != 0)
readerIndex(initialIndex)
return true
}
/**
* Sets a variable length integer at the current writerIndex and increases the writerIndex by up to 5 in this buffer.
*
* @see <a href="https://wiki.vg/Protocol#VarInt_and_VarLong">https://wiki.vg/Protocol#VarInt_and_VarLong</a>
*/
fun ByteBuf.writeVarInt(value: Int) {
var v = value
var part: Byte
while (true) {
part = (v and 0x7F).toByte()
v = v ushr 7
if (v != 0) {
part = (part.toInt() or 0x80).toByte()
}
writeByte(part.toInt())
if (v == 0) {
break
}
}
}
/**
* Gets a variable length UTF-8 encoded string at the current readerIndex and increases the readerIndex in this
* buffer by the number of bytes read.
*
* @see <a href="https://wiki.vg/Protocol#Data_types">https://wiki.vg/Protocol#Data_types</a>
*/
fun ByteBuf.readString(): String {
val length = readVarInt()
val bytes = ByteArray(length)
readBytes(bytes)
return bytes.toString(Charsets.UTF_8)
}
/**
* Sets a variable length UTF-8 encoded string at the current writerIndex and increases the writerIndex in this
* buffer by the number of bytes written.
*
* @see <a href="https://wiki.vg/Protocol#Data_types">https://wiki.vg/Protocol#Data_types</a>
*/
fun ByteBuf.writeString(value: String) {
val bytes = value.toByteArray(Charsets.UTF_8)
writeVarInt(bytes.size)
writeBytes(bytes)
}
}

View file

@ -1,16 +1,17 @@
package space.blokk.net package space.blokk.net
import space.blokk.events.EventBus import space.blokk.events.EventTarget
import space.blokk.net.events.SessionEvent import space.blokk.net.events.SessionEvent
import space.blokk.net.protocols.OutgoingPacket import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.Protocol import space.blokk.net.protocols.Protocol
import java.net.InetAddress import java.net.InetAddress
interface Session { interface Session: EventTarget<SessionEvent> {
/**
* The protocol this session is currently using.
*/
var currentProtocol: Protocol var currentProtocol: Protocol
val address: InetAddress val address: InetAddress
fun send(packet: OutgoingPacket) fun send(packet: OutgoingPacket)
val eventBus: EventBus<SessionEvent>
} }

View file

@ -1,7 +0,0 @@
package space.blokk.net
import space.blokk.events.Listener
interface SessionContainer: Iterable<Session> {
fun registerGlobalListener(listener: Listener)
}

View file

@ -1,5 +1,6 @@
package space.blokk.net.events package space.blokk.net.events
import space.blokk.events.Event import space.blokk.events.Event
import space.blokk.net.Session
abstract class SessionEvent: Event() abstract class SessionEvent(val session: Session): Event()

View file

@ -1,8 +1,9 @@
package space.blokk.net.events package space.blokk.net.events
import space.blokk.events.Cancellable import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.IncomingPacket import space.blokk.net.protocols.IncomingPacket
class SessionPacketReceivedEvent(var packet: IncomingPacket): SessionEvent(), Cancellable { class SessionPacketReceivedEvent(session: Session, var packet: IncomingPacket): SessionEvent(session), Cancellable {
override var isCancelled = false override var isCancelled = false
} }

View file

@ -1,8 +1,9 @@
package space.blokk.net.events package space.blokk.net.events
import space.blokk.events.Cancellable import space.blokk.events.Cancellable
import space.blokk.net.Session
import space.blokk.net.protocols.OutgoingPacket import space.blokk.net.protocols.OutgoingPacket
class SessionPacketSendEvent(var packet: OutgoingPacket): SessionEvent(), Cancellable { class SessionPacketSendEvent(session: Session, var packet: OutgoingPacket): SessionEvent(session), Cancellable {
override var isCancelled = false override var isCancelled = false
} }

View file

@ -1,6 +1,6 @@
package space.blokk.net.protocols package space.blokk.net.protocols
abstract class Protocol(packets: Set<PacketCompanion<*>>) { abstract class Protocol internal constructor(packets: Set<PacketCompanion<*>>) {
val incomingPackets = packets.filterIsInstance<IncomingPacketCompanion<*>>() val incomingPackets = packets.filterIsInstance<IncomingPacketCompanion<*>>()
val incomingPacketsByID = incomingPackets.mapToIDMap() val incomingPacketsByID = incomingPackets.mapToIDMap()
val outgoingPackets = packets.filterIsInstance<OutgoingPacketCompanion<*>>() val outgoingPackets = packets.filterIsInstance<OutgoingPacketCompanion<*>>()

View file

@ -6,5 +6,5 @@ import space.blokk.net.protocols.status.StatusProtocol
object Protocols { object Protocols {
val all = setOf(HandshakingProtocol, StatusProtocol, LoginProtocol) val all = setOf(HandshakingProtocol, StatusProtocol, LoginProtocol)
fun validate() = all.forEach(Protocol::validate) internal fun validate() = all.forEach(Protocol::validate)
} }

View file

@ -1,32 +1,33 @@
package space.blokk.net.protocols.handshaking package space.blokk.net.protocols.handshaking
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.Session import space.blokk.net.Session
import space.blokk.net.protocols.IncomingPacket import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.net.protocols.login.LoginProtocol import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.status.StatusProtocol import space.blokk.net.protocols.status.StatusProtocol
import space.blokk.net.readString
import space.blokk.net.readVarInt
data class HandshakePacket( data class HandshakePacket(
val protocolVersion: Int, val protocolVersion: Int,
val serverAddress: String, val serverAddress: String,
val serverPort: Int, val serverPort: Int,
val login: Boolean val isLoginAttempt: Boolean
): IncomingPacket() { ): IncomingPacket() {
override fun handle(session: Session) { override fun handle(session: Session) {
session.currentProtocol = if (login) LoginProtocol else StatusProtocol session.currentProtocol = if (isLoginAttempt) LoginProtocol else StatusProtocol
} }
companion object: IncomingPacketCompanion<HandshakePacket>(0x00, HandshakePacket::class) { companion object: IncomingPacketCompanion<HandshakePacket>(0x00, HandshakePacket::class) {
override fun decode(msg: ByteBuf): HandshakePacket { override fun decode(msg: ByteBuf): HandshakePacket {
return HandshakePacket( return with(MinecraftDataTypes) {
HandshakePacket(
protocolVersion = msg.readVarInt(), protocolVersion = msg.readVarInt(),
serverAddress = msg.readString(), serverAddress = msg.readString(),
serverPort = msg.readUnsignedShort(), serverPort = msg.readUnsignedShort(),
login = msg.readVarInt() == 2 isLoginAttempt = msg.readVarInt() == 2
) )
} }
} }
}
} }

View file

@ -3,9 +3,9 @@ package space.blokk.net.protocols.status
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import space.blokk.chat.TextComponent import space.blokk.chat.TextComponent
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.OutgoingPacket import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.writeString
import java.util.* import java.util.*
data class ResponsePacket( data class ResponsePacket(
@ -29,6 +29,7 @@ data class ResponsePacket(
} }
override fun encode(dst: ByteBuf) { override fun encode(dst: ByteBuf) {
with(MinecraftDataTypes) {
dst.writeString(gson.toJson(mapOf( dst.writeString(gson.toJson(mapOf(
"version" to mapOf( "version" to mapOf(
"name" to versionName, "name" to versionName,
@ -39,8 +40,13 @@ data class ResponsePacket(
"favicon" to favicon "favicon" to favicon
))) )))
} }
}
data class Players(val max: Int, val online: Int, val sample: List<SampleEntry>) { data class Players(val max: Int, val online: Int, val sample: List<SampleEntry>) {
/**
* @param name The name of the player. You can use [FormattingCode](space.blokk.chat.FormattingCode)s
* @param id The UUID of the player as a string. You can use a random UUID as it doesn't get validated by the Minecraft client
*/
data class SampleEntry(val name: String, val id: String) data class SampleEntry(val name: String, val id: String)
} }

View file

@ -1,10 +1,12 @@
package space.blokk.server package space.blokk.server
import space.blokk.events.EventBus import space.blokk.events.EventBus
import space.blokk.events.EventTarget
import space.blokk.server.events.ServerEvent import space.blokk.server.events.ServerEvent
import space.blokk.utils.Logger import space.blokk.utils.Logger
interface Server { interface Server: EventTarget<ServerEvent> {
val logger: Logger val logger: Logger
val eventBus: EventBus<ServerEvent>
override val eventBus: EventBus<ServerEvent>
} }

View file

@ -1,12 +1,13 @@
package space.blokk.net package space.blokk.net
import space.blokk.events.EventTargetGroup
import space.blokk.events.Listener import space.blokk.events.Listener
class BlokkSessionContainer: SessionContainer { class BlokkSessionContainer: EventTargetGroup<Session> {
private val sessions = mutableSetOf<Session>() private val sessions = mutableSetOf<Session>()
private val listeners = mutableSetOf<Listener>() private val listeners = mutableSetOf<Listener>()
override fun registerGlobalListener(listener: Listener) { override fun registerListener(listener: Listener) {
sessions.forEach { it.eventBus.register(listener) } sessions.forEach { it.eventBus.register(listener) }
listeners.add(listener) listeners.add(listener)
} }

View file

@ -8,13 +8,15 @@ class FramingCodec: ByteToMessageCodec<ByteBuf>() {
private var currentLength: Int? = null private var currentLength: Int? = null
override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) { override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) {
out.writeVarInt(msg.readableBytes()) with(MinecraftDataTypes) { out.writeVarInt(msg.readableBytes()) }
msg.readerIndex(0) msg.readerIndex(0)
out.writeBytes(msg) out.writeBytes(msg)
} }
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) { override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
with(MinecraftDataTypes) {
var length = currentLength var length = currentLength
if (length == null) { if (length == null) {
if (msg.varIntReadable()) { if (msg.varIntReadable()) {
length = msg.readVarInt() length = msg.readVarInt()
@ -28,4 +30,5 @@ class FramingCodec: ByteToMessageCodec<ByteBuf>() {
currentLength = null currentLength = null
} }
} }
}
} }

View file

@ -25,13 +25,13 @@ class PacketCodec(private val blokkSocketServer: BlokkSocketServer): MessageToMe
override fun encode(ctx: ChannelHandlerContext, msg: PacketMessage, out: MutableList<Any>) { override fun encode(ctx: ChannelHandlerContext, msg: PacketMessage, out: MutableList<Any>) {
if (msg.packet !is OutgoingPacket) throw Error("Only clientbound packets can be sent") if (msg.packet !is OutgoingPacket) throw Error("Only clientbound packets can be sent")
val buffer = ctx.alloc().buffer() val buffer = ctx.alloc().buffer()
buffer.writeVarInt(msg.packetCompanion.id) with(MinecraftDataTypes) { buffer.writeVarInt(msg.packetCompanion.id) }
msg.packet.encode(buffer) msg.packet.encode(buffer)
out.add(buffer) out.add(buffer)
} }
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) { override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
val packetID = msg.readVarInt() val packetID = with(MinecraftDataTypes) { msg.readVarInt() }
val data = msg.readBytes(msg.readableBytes()) val data = msg.readBytes(msg.readableBytes())
val packetCompanion = session.currentProtocol.incomingPacketsByID[packetID] val packetCompanion = session.currentProtocol.incomingPacketsByID[packetID]

View file

@ -3,6 +3,7 @@ package space.blokk.net
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import space.blokk.BlokkServer import space.blokk.BlokkServer
import space.blokk.events.ifNotCancelled
import space.blokk.net.events.SessionPacketReceivedEvent import space.blokk.net.events.SessionPacketReceivedEvent
import space.blokk.net.protocols.IncomingPacket import space.blokk.net.protocols.IncomingPacket
@ -11,8 +12,9 @@ class PacketMessageHandler: SimpleChannelInboundHandler<PacketMessage>() {
if (msg.packet !is IncomingPacket) throw Error("Only serverbound packets can be handled") if (msg.packet !is IncomingPacket) throw Error("Only serverbound packets can be handled")
BlokkServer.instance.blokkSocketServer.logger.debug { "Packet received: ${msg.packet}" } BlokkServer.instance.blokkSocketServer.logger.debug { "Packet received: ${msg.packet}" }
if (!msg.session.eventBus.emit(SessionPacketReceivedEvent(msg.packet)).isCancelled) msg.session.eventBus.emit(SessionPacketReceivedEvent(msg.session, msg.packet)).ifNotCancelled {
msg.packet.handle(msg.session) msg.packet.handle(msg.session)
}
// TODO: Disconnect when invalid data is received // TODO: Disconnect when invalid data is received
} }