Archived
1
0
Fork 0

Initial commit

This commit is contained in:
Moritz Ruth 2020-08-01 23:55:31 +02:00
commit cdfc11344d
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
48 changed files with 1306 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# Project exclude paths
/.gradle/
/blokk-api/build/
/blokk-server/build/

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 The Blokk contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,41 @@
plugins {
kotlin("jvm")
}
group = rootProject.group
version = rootProject.version
repositories {
mavenCentral()
jcenter()
}
val spekVersion = "2.0.12"
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation("com.google.code.gson:gson:2.8.6")
api("org.slf4j:slf4j-api:1.7.30")
api("io.netty:netty-buffer:4.1.50.Final")
testImplementation("io.strikt:strikt-core:0.26.1")
testImplementation("org.spekframework.spek2:spek-dsl-jvm:$spekVersion")
testRuntimeOnly("org.spekframework.spek2:spek-runner-junit5:$spekVersion")
testRuntimeOnly(kotlin("reflect"))
}
tasks {
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
test {
useJUnitPlatform {
includeEngines("spek2")
}
}
}

View file

@ -0,0 +1,5 @@
package space.blokk
object Blokk {
lateinit var server: Server
}

View file

@ -0,0 +1,7 @@
package space.blokk
import space.blokk.utils.Logger
interface Server {
val logger: Logger
}

View file

@ -0,0 +1,24 @@
package space.blokk.chat
import kotlin.reflect.KClass
sealed class ChatComponent {
abstract val extra: ChatComponent?
fun getExtraTypes(): List<KClass<out ChatComponent>> {
val types = mutableListOf<KClass<out ChatComponent>>()
var current = extra
while (current != null) {
types.add(current::class)
current = current.extra
}
return types.toList()
}
}
data class TextComponent(val text: String, override val extra: ChatComponent? = null): ChatComponent() {
companion object {
infix fun of(text: String) = TextComponent(text)
}
}

View file

@ -0,0 +1,33 @@
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]).
*/
enum class FormattingCode(private val char: Char) {
BLACK('0'),
DARK_BLUE('1'),
DARK_GREEN('2'),
DARK_AQUA('3'),
DARK_RED('4'),
DARK_PURPLE('5'),
GOLD('6'),
GRAY('7'),
DARK_GRAY('8'),
BLUE('9'),
GREEN('a'),
AQUA('b'),
RED('c'),
LIGHT_PURPLE('d'),
YELLOW('e'),
WHITE('f'),
OBFUSCATED('k'),
BOLD('l'),
STRIKETHROUGH('m'),
UNDERLINE('n'),
ITALIC('o'),
RESET('r');
override fun toString(): String = "§$char"
}

View file

@ -0,0 +1,5 @@
package space.blokk.events
interface Cancellable {
var isCancelled: Boolean
}

View file

@ -0,0 +1,4 @@
package space.blokk.events
abstract class Event {
}

View file

@ -0,0 +1,51 @@
package space.blokk.events
import space.blokk.plugins.Plugin
import java.lang.reflect.Method
import kotlin.reflect.KClass
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
*/
private val handlers = mutableListOf<Handler>()
fun emit(event: EventT) {
handlers.filter { it.eventType.isInstance(event) }.forEach { it.fn.invoke(it.listener, event) }
}
fun register(listener: Listener) {
val handlersOfListener = listener::class.java.methods
.mapNotNull { method -> method.getAnnotation(EventHandler::class.java)?.let { method to it } }
.toMap()
for ((method, data) in handlersOfListener) {
if (method.parameters.count() != 1)
throw InvalidEventHandlerException("${method.name} must have exactly one parameter")
val type = method.parameterTypes[0]
if (!eventType.isAssignableFrom(type))
throw InvalidEventHandlerException("${method.name}'s first parameter type is incompatible with the " +
"one required by the EventBus")
@Suppress("UNCHECKED_CAST")
val handler = Handler(type as Class<out Event>, listener, method, Plugin.getCalling(), data.priority)
val insertIndex = handlers.indexOfLast { it.priority.ordinal <= handler.priority.ordinal } + 1
handlers.add(insertIndex, handler)
println(handlers)
}
}
class InvalidEventHandlerException(message: String): Exception(message)
private data class Handler(
val eventType: Class<out Event>,
val listener: Listener,
val fn: Method,
val plugin: Plugin?,
val priority: EventPriority
)
}

View file

@ -0,0 +1,3 @@
package space.blokk.events
annotation class EventHandler(val priority: EventPriority = EventPriority.NORMAL)

View file

@ -0,0 +1,12 @@
package space.blokk.events
enum class EventPriority {
LOWEST,
LOW,
LOWER,
NORMAL,
HIGHER,
HIGH,
HIGHEST,
MONITOR
}

View file

@ -0,0 +1,3 @@
package space.blokk.events
interface Listener

View file

@ -0,0 +1,70 @@
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,7 @@
package space.blokk.net
import space.blokk.events.Cancellable
open class PacketReceivedEvent: SessionEvent(), Cancellable {
override var isCancelled = false
}

View file

@ -0,0 +1,7 @@
package space.blokk.net
import space.blokk.net.protocols.ClientboundPacket
class PacketWithResponseReceivedEvent<ResponseT: ClientboundPacket>(
var response: ResponseT
): PacketReceivedEvent()

View file

@ -0,0 +1,18 @@
package space.blokk.net
import space.blokk.events.Event
import space.blokk.events.EventBus
import space.blokk.net.protocols.ClientboundPacket
import space.blokk.net.protocols.Protocol
import java.net.InetAddress
interface Session {
var currentProtocol: Protocol
val address: InetAddress
fun send(packet: ClientboundPacket)
val eventsManager: EventBus<SessionEvent>
}
open class SessionEvent: Event()

View file

@ -0,0 +1,29 @@
package space.blokk.net.protocols
import io.netty.buffer.ByteBuf
import space.blokk.net.Session
import kotlin.reflect.KClass
abstract class Packet
abstract class ServerboundPacket: Packet() {
abstract fun handle(session: Session)
}
abstract class ClientboundPacket: Packet() {
abstract fun encode(dst: ByteBuf)
}
sealed class PacketCompanion<T: Packet>(val id: Int, val packetType: KClass<T>)
abstract class ServerboundPacketCompanion<T: ServerboundPacket>(
id: Int,
packetType: KClass<T>
): PacketCompanion<T>(id, packetType) {
abstract fun decode(msg: ByteBuf): T
}
abstract class ClientboundPacketCompanion<T: ClientboundPacket>(
id: Int,
packetType: KClass<T>
): PacketCompanion<T>(id, packetType)

View file

@ -0,0 +1,25 @@
package space.blokk.net.protocols
abstract class Protocol(packets: Set<PacketCompanion<*>>) {
val serverboundPackets = packets.filterIsInstance<ServerboundPacketCompanion<*>>()
val serverboundPacketsByID = serverboundPackets.mapToIDMap()
val clientboundPackets = packets.filterIsInstance<ClientboundPacketCompanion<*>>()
val clientboundPacketsById = clientboundPackets.mapToIDMap()
val packetCompanionsByPacketType = packets.map { it.packetType to it }.toMap()
private fun <T: PacketCompanion<*>> Iterable<T>.mapToIDMap() = map { it.id to it }.toMap()
fun validate() {
ensureDistinctIDs(serverboundPackets, "serverbound")
ensureDistinctIDs(clientboundPackets, "clientbound")
}
private fun ensureDistinctIDs(packets: Iterable<PacketCompanion<*>>, type: String) {
packets.groupBy { it.id }.forEach { (id, p) ->
if (p.count() > 1) {
val packetsString = p.joinToString(", ", limit = 4) { it.packetType.simpleName.toString() }
throw Error("Multiple $type packets use the same ID (0x${id.toString(16)}): $packetsString")
}
}
}
}

View file

@ -0,0 +1,10 @@
package space.blokk.net.protocols
import space.blokk.net.protocols.handshaking.HandshakingProtocol
import space.blokk.net.protocols.login.LoginProtocol
import space.blokk.net.protocols.status.StatusProtocol
object Protocols {
val all = setOf(HandshakingProtocol, StatusProtocol, LoginProtocol)
fun validate() = all.forEach(Protocol::validate)
}

View file

@ -0,0 +1,39 @@
package space.blokk.net.protocols.handshaking
import io.netty.buffer.ByteBuf
import space.blokk.Blokk
import space.blokk.net.PacketReceivedEvent
import space.blokk.net.Session
import space.blokk.net.protocols.ServerboundPacket
import space.blokk.net.protocols.ServerboundPacketCompanion
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
): ServerboundPacket() {
override fun handle(session: Session) {
val event = PacketReceivedEvent()
session.eventsManager.emit(event)
if (!event.isCancelled) {
Blokk.server.logger.debug { "${session.address.hostAddress} initiated handshake. Is login: $login" }
session.currentProtocol = if (login) LoginProtocol else StatusProtocol
}
}
companion object: ServerboundPacketCompanion<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
)
}
}
}

View file

@ -0,0 +1,5 @@
package space.blokk.net.protocols.handshaking
import space.blokk.net.protocols.Protocol
object HandshakingProtocol : Protocol(setOf(HandshakePacket))

View file

@ -0,0 +1,6 @@
package space.blokk.net.protocols.login
import space.blokk.net.protocols.Protocol
object LoginProtocol: Protocol(setOf()) {
}

View file

@ -0,0 +1,18 @@
package space.blokk.net.protocols.status
import io.netty.buffer.ByteBuf
import space.blokk.net.Session
import space.blokk.net.protocols.ServerboundPacket
import space.blokk.net.protocols.ServerboundPacketCompanion
data class PingPacket(val payload: Long) : ServerboundPacket() {
override fun handle(session: Session) {
session.send(PongPacket(payload))
}
companion object : ServerboundPacketCompanion<PingPacket>(0x01, PingPacket::class) {
override fun decode(msg: ByteBuf): PingPacket {
return PingPacket(msg.readLong())
}
}
}

View file

@ -0,0 +1,13 @@
package space.blokk.net.protocols.status
import io.netty.buffer.ByteBuf
import space.blokk.net.protocols.ClientboundPacket
import space.blokk.net.protocols.ClientboundPacketCompanion
data class PongPacket(val payload: Long) : ClientboundPacket() {
override fun encode(dst: ByteBuf) {
dst.writeLong(payload)
}
companion object : ClientboundPacketCompanion<PongPacket>(0x01, PongPacket::class)
}

View file

@ -0,0 +1,29 @@
package space.blokk.net.protocols.status
import io.netty.buffer.ByteBuf
import space.blokk.chat.FormattingCode
import space.blokk.chat.TextComponent
import space.blokk.net.Session
import space.blokk.net.protocols.ServerboundPacket
import space.blokk.net.protocols.ServerboundPacketCompanion
import java.util.*
class RequestPacket: ServerboundPacket() {
override fun handle(session: Session) {
// TODO: Respond with the correct data
session.send(ResponsePacket(
versionName = "1.15.2",
protocolVersion = 578,
description = TextComponent of "nice",
players = ResponsePacket.Players(
10,
10,
listOf(ResponsePacket.Players.SampleEntry("${FormattingCode.AQUA}Gronkh", UUID.randomUUID().toString())
))
))
}
companion object : ServerboundPacketCompanion<RequestPacket>(0x00, RequestPacket::class) {
override fun decode(msg: ByteBuf): RequestPacket = RequestPacket()
}
}

View file

@ -0,0 +1,50 @@
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.protocols.ClientboundPacket
import space.blokk.net.protocols.ClientboundPacketCompanion
import space.blokk.net.writeString
import java.util.*
data class ResponsePacket(
val versionName: String,
val protocolVersion: Int,
val description: TextComponent,
val players: Players,
val favicon: String? = null
) : ClientboundPacket() {
constructor(
versionName: String,
protocolVersion: Int,
players: Players,
description: TextComponent,
favicon: ByteArray
): this(versionName, protocolVersion, description, players, Base64.getEncoder().encodeToString(favicon))
init {
if (description.getExtraTypes().find { it != TextComponent::class } != null)
throw Exception("description may only contain instances of TextComponent")
}
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
)))
}
data class Players(val max: Int, val online: Int, val sample: List<SampleEntry>) {
data class SampleEntry(val name: String, val id: String)
}
companion object : ClientboundPacketCompanion<ResponsePacket>(0x00, ResponsePacket::class) {
private val gson = GsonBuilder().disableHtmlEscaping().create()!!
}
}

View file

@ -0,0 +1,5 @@
package space.blokk.net.protocols.status
import space.blokk.net.protocols.Protocol
object StatusProtocol: Protocol(setOf(RequestPacket, ResponsePacket, PingPacket, PongPacket))

View file

@ -0,0 +1,11 @@
package space.blokk.plugins
interface Plugin {
companion object {
fun getCalling(): Plugin? {
// TODO: Return the last plugin in the stacktrace
// This could be useful: https://stackoverflow.com/questions/421280/how-do-i-find-the-caller-of-a-method-using-stacktrace-or-reflection
return null
}
}
}

View file

@ -0,0 +1,33 @@
package space.blokk.utils
import org.slf4j.LoggerFactory
class Logger(name: String) {
private val logger = LoggerFactory.getLogger(name)
infix fun error(msg: String) = logger.error(msg)
infix fun info(msg: String) = logger.info(msg)
infix fun warn(msg: String) = logger.warn(msg)
infix fun debug(msg: String) = logger.debug(msg)
infix fun trace(msg: String) = logger.trace(msg)
fun error(fn: () -> String) {
if (logger.isErrorEnabled) logger.error(fn())
}
fun info(fn: () -> String) {
if (logger.isErrorEnabled) logger.info(fn())
}
fun warn(fn: () -> String) {
if (logger.isErrorEnabled) logger.warn(fn())
}
fun debug(fn: () -> String) {
if (logger.isErrorEnabled) logger.debug(fn())
}
fun trace(fn: () -> String) {
if (logger.isErrorEnabled) logger.trace(fn())
}
}

View file

@ -0,0 +1,110 @@
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import space.blokk.events.*
import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import strikt.assertions.isFalse
import strikt.assertions.isSorted
import strikt.assertions.isTrue
private abstract class TestEvent: Event()
private class FirstEvent: TestEvent()
private class SecondEvent: TestEvent()
object EventBusTest: Spek({
describe("EventBus") {
val eventBus by memoized { EventBus(TestEvent::class) }
it("calls the handler exactly 1 time when the event is emitted 1 time") {
var calledCount = 0
eventBus.register(object : Listener {
@EventHandler fun onFirstEvent(event: FirstEvent) { calledCount++ }
})
eventBus.emit(FirstEvent())
expectThat(calledCount).isEqualTo(1)
}
it("calls the handler 3 times when the event is emitted 3 times") {
var calledCount = 0
eventBus.register(object : Listener {
@EventHandler fun onFirstEvent(event: FirstEvent) { calledCount++ }
})
eventBus.emit(FirstEvent())
eventBus.emit(FirstEvent())
eventBus.emit(FirstEvent())
expectThat(calledCount).isEqualTo(3)
}
it("calls the handlers for super types of the emitted event") {
var onTestEventCalled = false
var onFirstEventCalled = false
eventBus.register(object : Listener {
@EventHandler fun onTestEvent(event: TestEvent) { onTestEventCalled = true }
@EventHandler fun onFirstEvent(event: FirstEvent) { onFirstEventCalled = true }
})
eventBus.emit(FirstEvent())
expectThat(onTestEventCalled).isTrue()
expectThat(onFirstEventCalled).isTrue()
}
it("calls no handlers if no event is emitted") {
var onFirstEventCalled = false
var onSecondEventCalled = false
eventBus.register(object : Listener {
@EventHandler fun onFirstEvent(event: FirstEvent) { onFirstEventCalled = true }
@EventHandler fun onSecondEvent(event: SecondEvent) { onSecondEventCalled = true }
})
// No emit
expectThat(onFirstEventCalled).isFalse()
expectThat(onSecondEventCalled).isFalse()
}
it("throws an error if an event handler function has an invalid signature") {
expectThrows<EventBus.InvalidEventHandlerException> {
eventBus.register(object : Listener {
@EventHandler fun onEvent(coolParam: String) {}
})
}
expectThrows<EventBus.InvalidEventHandlerException> {
eventBus.register(object : Listener {
@EventHandler fun onFirstEvent(coolParam: String, event: FirstEvent) {}
})
}
expectThrows<EventBus.InvalidEventHandlerException> {
eventBus.register(object : Listener {
@EventHandler fun onFirstEvent(event: FirstEvent, coolParam: String) {}
})
}
}
it("calls handlers in the right order, sorted by priority") {
val order = mutableListOf<EventPriority>()
eventBus.register(object : Listener {
@EventHandler(EventPriority.HIGHEST) fun onFirstEventHighest(event: FirstEvent) { order.add(EventPriority.HIGHEST) }
@EventHandler(EventPriority.LOW) fun onFirstEventLow(event: FirstEvent) { order.add(EventPriority.LOW) }
@EventHandler(EventPriority.HIGHER) fun onFirstEventHigher(event: FirstEvent) { order.add(EventPriority.HIGHER) }
@EventHandler(EventPriority.LOWEST) fun onFirstEventLowest(event: FirstEvent) { order.add(EventPriority.LOWEST) }
@EventHandler(EventPriority.MONITOR) fun onFirstEventMonitor(event: FirstEvent) { order.add(EventPriority.MONITOR) }
@EventHandler(EventPriority.LOWER) fun onFirstEventLower(event: FirstEvent) { order.add(EventPriority.LOWER) }
@EventHandler(EventPriority.NORMAL) fun onFirstEventNormal(event: FirstEvent) { order.add(EventPriority.NORMAL) }
@EventHandler(EventPriority.HIGH) fun onFirstEventHigh(event: FirstEvent) { order.add(EventPriority.HIGH) }
})
eventBus.emit(FirstEvent())
expectThat(order).isSorted(Comparator.comparing<EventPriority, Int> { it.ordinal })
}
}
})

View file

@ -0,0 +1,37 @@
plugins {
kotlin("jvm")
id("com.github.johnrengelman.shadow") version "6.0.0"
}
group = rootProject.group
version = rootProject.version
repositories {
mavenCentral()
}
dependencies {
implementation(project(":blokk-api"))
implementation(kotlin("stdlib-jdk8"))
implementation("io.netty:netty-all:4.1.50.Final")
implementation("org.slf4j:slf4j-api:1.7.30")
implementation("ch.qos.logback:logback-classic:1.2.3")
testImplementation(kotlin("test-junit5"))
}
tasks {
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
shadowJar {
archiveClassifier.set(null as String?)
manifest {
this.attributes("Main-Class" to "space.blokk.BlokkServer")
this.attributes("Implementation-Version" to project.version)
}
}
}

View file

@ -0,0 +1,33 @@
package space.blokk
import space.blokk.net.BlokkSocketServer
import space.blokk.utils.Logger
class BlokkServer internal constructor(): Server {
override val logger = Logger("BlokkServer")
val host = "0.0.0.0"
val port = 25565
init {
instance = this
Blokk.server = this
}
fun start() {
logger info "Starting BlokkServer (${if (VERSION == "development") VERSION else "v$VERSION"})"
val blokkSocketServer = BlokkSocketServer(this)
blokkSocketServer.bind()
logger info "Listening on $host:$port"
}
companion object {
lateinit var instance: BlokkServer; private set
val VERSION = BlokkServer::class.java.`package`.implementationVersion ?: "development"
@JvmStatic
fun main(args: Array<String>) {
val server = BlokkServer()
server.start()
}
}
}

View file

@ -0,0 +1,26 @@
package space.blokk.net
import io.netty.channel.Channel
import io.netty.channel.ChannelException
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.handler.timeout.IdleStateHandler
import space.blokk.BlokkServer
class BlokkChannelInitializer(private val blokkSocketServer: BlokkSocketServer): ChannelInitializer<Channel>() {
private val logger = BlokkServer.instance.logger
override fun initChannel(ch: Channel) {
try {
ch.config().setOption(ChannelOption.IP_TOS, 0x18)
} catch (e: ChannelException) {
logger warn "Your OS does not support IP type of service"
}
ch.pipeline()
.addLast(IdleStateHandler(20, 15, 0))
.addLast(FramingCodec())
.addLast(PacketCodec(blokkSocketServer))
.addLast(PacketMessageHandler())
}
}

View file

@ -0,0 +1,27 @@
package space.blokk.net
import io.netty.channel.Channel
import space.blokk.Blokk
import space.blokk.events.EventBus
import space.blokk.net.protocols.ClientboundPacket
import space.blokk.net.protocols.Protocol
import space.blokk.net.protocols.handshaking.HandshakingProtocol
import java.net.InetAddress
import java.net.InetSocketAddress
class BlokkSession(private val channel: Channel) : Session {
override var currentProtocol: Protocol = HandshakingProtocol
override val address: InetAddress = (channel.remoteAddress() as InetSocketAddress).address
override val eventsManager = EventBus(SessionEvent::class)
override fun send(packet: ClientboundPacket) {
Blokk.server.logger.debug { "Sending packet: $packet" }
val cf = channel.writeAndFlush(PacketMessage(this, packet))
cf.addListener { future ->
if (!future.isSuccess) {
Blokk.server.logger.error { "Packet send failed: ${future.cause()}" }
}
}
}
}

View file

@ -0,0 +1,49 @@
package space.blokk.net
import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelOption
import io.netty.channel.epoll.Epoll
import io.netty.channel.epoll.EpollEventLoopGroup
import io.netty.channel.epoll.EpollServerSocketChannel
import io.netty.channel.kqueue.KQueue
import io.netty.channel.kqueue.KQueueEventLoopGroup
import io.netty.channel.kqueue.KQueueServerSocketChannel
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioServerSocketChannel
import space.blokk.BlokkServer
import space.blokk.net.protocols.Protocols
class BlokkSocketServer(private val blokkServer: BlokkServer) {
private val bossGroup = createEventLoopGroup()
private val workerGroup = createEventLoopGroup()
private val bootstrap: ServerBootstrap = ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(when {
EPOLL_AVAILABLE -> EpollServerSocketChannel::class.java
KQUEUE_AVAILABLE -> KQueueServerSocketChannel::class.java
else -> NioServerSocketChannel::class.java
})
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(BlokkChannelInitializer(this))
val sessions = mutableSetOf<BlokkSession>()
init {
Protocols.validate()
}
fun bind() {
bootstrap.bind(blokkServer.host, blokkServer.port)
}
companion object {
private val EPOLL_AVAILABLE = Epoll.isAvailable()
private val KQUEUE_AVAILABLE = KQueue.isAvailable()
private fun createEventLoopGroup() = when {
EPOLL_AVAILABLE -> EpollEventLoopGroup()
KQUEUE_AVAILABLE -> KQueueEventLoopGroup()
else -> NioEventLoopGroup()
}
}
}

View file

@ -0,0 +1,31 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.ByteToMessageCodec
class FramingCodec: ByteToMessageCodec<ByteBuf>() {
private var currentLength: Int? = null
override fun encode(ctx: ChannelHandlerContext, msg: ByteBuf, out: ByteBuf) {
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
}
if (msg.readableBytes() >= length) {
out.add(msg.readBytes(length))
currentLength = null
}
}
}

View file

@ -0,0 +1,39 @@
package space.blokk.net
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelHandlerContext
import io.netty.handler.codec.MessageToMessageCodec
import space.blokk.net.protocols.ClientboundPacket
import java.io.IOException
class PacketCodec(private val blokkSocketServer: BlokkSocketServer): MessageToMessageCodec<ByteBuf, PacketMessage>() {
private lateinit var session: BlokkSession
override fun channelActive(ctx: ChannelHandlerContext) {
session = BlokkSession(ctx.channel())
blokkSocketServer.sessions.add(session)
}
override fun channelInactive(ctx: ChannelHandlerContext) {
blokkSocketServer.sessions.remove(session)
}
override fun encode(ctx: ChannelHandlerContext, msg: PacketMessage, out: MutableList<Any>) {
if (msg.packet !is ClientboundPacket) throw Error("Only clientbound packets can be sent")
val buffer = ctx.alloc().buffer()
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 data = msg.readBytes(msg.readableBytes())
val packetCompanion = session.currentProtocol.serverboundPacketsByID[packetID]
?: throw IOException("Client sent a packet with an invalid ID: $packetID")
out.add(PacketMessage(session, packetCompanion.decode(data)))
}
}

View file

@ -0,0 +1,11 @@
package space.blokk.net
import space.blokk.net.protocols.Packet
data class PacketMessage(val session: BlokkSession, val packet: Packet) {
val packetCompanion by lazy {
session.currentProtocol.packetCompanionsByPacketType[packet::class]
?: throw Exception("No packet companion found for this packet type. " +
"This can happen if the packet is not part of the current protocol.")
}
}

View file

@ -0,0 +1,15 @@
package space.blokk.net
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler
import space.blokk.Blokk
import space.blokk.net.protocols.ServerboundPacket
class PacketMessageHandler: SimpleChannelInboundHandler<PacketMessage>() {
override fun channelRead0(ctx: ChannelHandlerContext, msg: PacketMessage) {
if (msg.packet !is ServerboundPacket) throw Error("Only serverbound packets can be handled")
Blokk.server.logger.debug { "Packet received: ${msg.packet}" }
msg.packet.handle(msg.session)
// TODO: Disconnect when invalid data is received
}
}

View file

@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %thread %-5level [%logger{36}] %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
<logger name="io.netty" level="WARN"/>
</configuration>

24
build.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
plugins {
kotlin("jvm") version "1.3.71"
}
group = "space.blokk"
version = "0.0.1"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
}
tasks {
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
}

1
gradle.properties Normal file
View file

@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

183
gradlew vendored Executable file
View file

@ -0,0 +1,183 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

103
gradlew.bat vendored Normal file
View file

@ -0,0 +1,103 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

12
settings.gradle.kts Normal file
View file

@ -0,0 +1,12 @@
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "blokk"
include(":blokk-api")
include(":blokk-server")