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
import space.blokk.net.SessionContainer
import space.blokk.events.EventTargetGroup
import space.blokk.net.Session
import space.blokk.server.Server
interface BlokkProvider {
val server: Server
val sessions: SessionContainer
/**
* [EventTargetGroup] instance containing all sessions connected to the server.
*/
val sessions: EventTargetGroup<Session>
}
object Blokk: BlokkProvider {
// Is assigned by BlokkServer using reflection
private var provider: BlokkProvider? = null
override val server get() = provider!!.server
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() {
companion object {
/**
* Creates a new [TextComponent] instance using [text] and returns it.
*/
infix fun of(text: String) = TextComponent(text)
}
}

View file

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

View file

@ -3,3 +3,13 @@ package space.blokk.events
interface Cancellable {
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
/**
* 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>()
/**
* 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 {
handlers.filter { it.eventType.isInstance(event) }.forEach { it.fn.invoke(it.listener, 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) {
val handlersOfListener = listener::class.java.methods
.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(
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
import space.blokk.events.EventBus
import space.blokk.events.EventTarget
import space.blokk.net.events.SessionEvent
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.Protocol
import java.net.InetAddress
interface Session {
interface Session: EventTarget<SessionEvent> {
/**
* The protocol this session is currently using.
*/
var currentProtocol: Protocol
val address: InetAddress
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
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
import space.blokk.events.Cancellable
import space.blokk.net.Session
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
}

View file

@ -1,8 +1,9 @@
package space.blokk.net.events
import space.blokk.events.Cancellable
import space.blokk.net.Session
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
}

View file

@ -1,6 +1,6 @@
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 incomingPacketsByID = incomingPackets.mapToIDMap()
val outgoingPackets = packets.filterIsInstance<OutgoingPacketCompanion<*>>()

View file

@ -6,5 +6,5 @@ import space.blokk.net.protocols.status.StatusProtocol
object Protocols {
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
import io.netty.buffer.ByteBuf
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.Session
import space.blokk.net.protocols.IncomingPacket
import space.blokk.net.protocols.IncomingPacketCompanion
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.status.StatusProtocol
import space.blokk.net.readString
import space.blokk.net.readVarInt
data class HandshakePacket(
val protocolVersion: Int,
val serverAddress: String,
val serverPort: Int,
val login: Boolean
val isLoginAttempt: Boolean
): IncomingPacket() {
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) {
override fun decode(msg: ByteBuf): HandshakePacket {
return HandshakePacket(
protocolVersion = msg.readVarInt(),
serverAddress = msg.readString(),
serverPort = msg.readUnsignedShort(),
login = msg.readVarInt() == 2
)
return with(MinecraftDataTypes) {
HandshakePacket(
protocolVersion = msg.readVarInt(),
serverAddress = msg.readString(),
serverPort = msg.readUnsignedShort(),
isLoginAttempt = msg.readVarInt() == 2
)
}
}
}
}

View file

@ -3,9 +3,9 @@ package space.blokk.net.protocols.status
import com.google.gson.GsonBuilder
import io.netty.buffer.ByteBuf
import space.blokk.chat.TextComponent
import space.blokk.net.MinecraftDataTypes
import space.blokk.net.protocols.OutgoingPacket
import space.blokk.net.protocols.OutgoingPacketCompanion
import space.blokk.net.writeString
import java.util.*
data class ResponsePacket(
@ -29,18 +29,24 @@ data class ResponsePacket(
}
override fun encode(dst: ByteBuf) {
dst.writeString(gson.toJson(mapOf(
"version" to mapOf(
"name" to versionName,
"protocol" to protocolVersion
),
"players" to players,
"description" to description,
"favicon" to favicon
)))
with(MinecraftDataTypes) {
dst.writeString(gson.toJson(mapOf(
"version" to mapOf(
"name" to versionName,
"protocol" to protocolVersion
),
"players" to players,
"description" to description,
"favicon" to favicon
)))
}
}
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)
}

View file

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

View file

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

View file

@ -8,24 +8,27 @@ class FramingCodec: ByteToMessageCodec<ByteBuf>() {
private var currentLength: Int? = null
override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) {
out.writeVarInt(msg.readableBytes())
with(MinecraftDataTypes) { out.writeVarInt(msg.readableBytes()) }
msg.readerIndex(0)
out.writeBytes(msg)
}
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
var length = currentLength
if (length == null) {
if (msg.varIntReadable()) {
length = msg.readVarInt()
currentLength = length
}
else return
}
with(MinecraftDataTypes) {
var length = currentLength
if (msg.readableBytes() >= length) {
out.add(msg.readBytes(length))
currentLength = null
if (length == null) {
if (msg.varIntReadable()) {
length = msg.readVarInt()
currentLength = length
}
else return
}
if (msg.readableBytes() >= length) {
out.add(msg.readBytes(length))
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>) {
if (msg.packet !is OutgoingPacket) throw Error("Only clientbound packets can be sent")
val buffer = ctx.alloc().buffer()
buffer.writeVarInt(msg.packetCompanion.id)
with(MinecraftDataTypes) { buffer.writeVarInt(msg.packetCompanion.id) }
msg.packet.encode(buffer)
out.add(buffer)
}
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 packetCompanion = session.currentProtocol.incomingPacketsByID[packetID]

View file

@ -3,6 +3,7 @@ package space.blokk.net
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import space.blokk.BlokkServer
import space.blokk.events.ifNotCancelled
import space.blokk.net.events.SessionPacketReceivedEvent
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")
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)
}
// TODO: Disconnect when invalid data is received
}