Initial commit
This commit is contained in:
commit
cdfc11344d
48 changed files with 1306 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# Project exclude paths
|
||||||
|
/.gradle/
|
||||||
|
/blokk-api/build/
|
||||||
|
/blokk-server/build/
|
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
41
blokk-api/build.gradle.kts
Normal file
41
blokk-api/build.gradle.kts
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
blokk-api/src/main/kotlin/space/blokk/Blokk.kt
Normal file
5
blokk-api/src/main/kotlin/space/blokk/Blokk.kt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package space.blokk
|
||||||
|
|
||||||
|
object Blokk {
|
||||||
|
lateinit var server: Server
|
||||||
|
}
|
7
blokk-api/src/main/kotlin/space/blokk/Server.kt
Normal file
7
blokk-api/src/main/kotlin/space/blokk/Server.kt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package space.blokk
|
||||||
|
|
||||||
|
import space.blokk.utils.Logger
|
||||||
|
|
||||||
|
interface Server {
|
||||||
|
val logger: Logger
|
||||||
|
}
|
24
blokk-api/src/main/kotlin/space/blokk/chat/ChatComponent.kt
Normal file
24
blokk-api/src/main/kotlin/space/blokk/chat/ChatComponent.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
33
blokk-api/src/main/kotlin/space/blokk/chat/FormattingCode.kt
Normal file
33
blokk-api/src/main/kotlin/space/blokk/chat/FormattingCode.kt
Normal 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"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package space.blokk.events
|
||||||
|
|
||||||
|
interface Cancellable {
|
||||||
|
var isCancelled: Boolean
|
||||||
|
}
|
4
blokk-api/src/main/kotlin/space/blokk/events/Event.kt
Normal file
4
blokk-api/src/main/kotlin/space/blokk/events/Event.kt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package space.blokk.events
|
||||||
|
|
||||||
|
abstract class Event {
|
||||||
|
}
|
51
blokk-api/src/main/kotlin/space/blokk/events/EventBus.kt
Normal file
51
blokk-api/src/main/kotlin/space/blokk/events/EventBus.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package space.blokk.events
|
||||||
|
|
||||||
|
annotation class EventHandler(val priority: EventPriority = EventPriority.NORMAL)
|
|
@ -0,0 +1,12 @@
|
||||||
|
package space.blokk.events
|
||||||
|
|
||||||
|
enum class EventPriority {
|
||||||
|
LOWEST,
|
||||||
|
LOW,
|
||||||
|
LOWER,
|
||||||
|
NORMAL,
|
||||||
|
HIGHER,
|
||||||
|
HIGH,
|
||||||
|
HIGHEST,
|
||||||
|
MONITOR
|
||||||
|
}
|
3
blokk-api/src/main/kotlin/space/blokk/events/Listener.kt
Normal file
3
blokk-api/src/main/kotlin/space/blokk/events/Listener.kt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package space.blokk.events
|
||||||
|
|
||||||
|
interface Listener
|
|
@ -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)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package space.blokk.net
|
||||||
|
|
||||||
|
import space.blokk.events.Cancellable
|
||||||
|
|
||||||
|
open class PacketReceivedEvent: SessionEvent(), Cancellable {
|
||||||
|
override var isCancelled = false
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package space.blokk.net
|
||||||
|
|
||||||
|
import space.blokk.net.protocols.ClientboundPacket
|
||||||
|
|
||||||
|
class PacketWithResponseReceivedEvent<ResponseT: ClientboundPacket>(
|
||||||
|
var response: ResponseT
|
||||||
|
): PacketReceivedEvent()
|
18
blokk-api/src/main/kotlin/space/blokk/net/Session.kt
Normal file
18
blokk-api/src/main/kotlin/space/blokk/net/Session.kt
Normal 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()
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package space.blokk.net.protocols.handshaking
|
||||||
|
|
||||||
|
import space.blokk.net.protocols.Protocol
|
||||||
|
|
||||||
|
object HandshakingProtocol : Protocol(setOf(HandshakePacket))
|
|
@ -0,0 +1,6 @@
|
||||||
|
package space.blokk.net.protocols.login
|
||||||
|
|
||||||
|
import space.blokk.net.protocols.Protocol
|
||||||
|
|
||||||
|
object LoginProtocol: Protocol(setOf()) {
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()!!
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package space.blokk.net.protocols.status
|
||||||
|
|
||||||
|
import space.blokk.net.protocols.Protocol
|
||||||
|
|
||||||
|
object StatusProtocol: Protocol(setOf(RequestPacket, ResponsePacket, PingPacket, PongPacket))
|
11
blokk-api/src/main/kotlin/space/blokk/plugins/Plugin.kt
Normal file
11
blokk-api/src/main/kotlin/space/blokk/plugins/Plugin.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
blokk-api/src/main/kotlin/space/blokk/utils/Logger.kt
Normal file
33
blokk-api/src/main/kotlin/space/blokk/utils/Logger.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
110
blokk-api/src/test/kotlin/EventBusTest.kt
Normal file
110
blokk-api/src/test/kotlin/EventBusTest.kt
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
37
blokk-server/build.gradle.kts
Normal file
37
blokk-server/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
blokk-server/src/main/kotlin/space/blokk/BlokkServer.kt
Normal file
33
blokk-server/src/main/kotlin/space/blokk/BlokkServer.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
27
blokk-server/src/main/kotlin/space/blokk/net/BlokkSession.kt
Normal file
27
blokk-server/src/main/kotlin/space/blokk/net/BlokkSession.kt
Normal 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()}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
blokk-server/src/main/kotlin/space/blokk/net/FramingCodec.kt
Normal file
31
blokk-server/src/main/kotlin/space/blokk/net/FramingCodec.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
blokk-server/src/main/kotlin/space/blokk/net/PacketCodec.kt
Normal file
39
blokk-server/src/main/kotlin/space/blokk/net/PacketCodec.kt
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
12
blokk-server/src/main/resources/logback.xml
Normal file
12
blokk-server/src/main/resources/logback.xml
Normal 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
24
build.gradle.kts
Normal 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
1
gradle.properties
Normal file
|
@ -0,0 +1 @@
|
||||||
|
kotlin.code.style=official
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
183
gradlew
vendored
Executable 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
103
gradlew.bat
vendored
Normal 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
12
settings.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = "blokk"
|
||||||
|
|
||||||
|
include(":blokk-api")
|
||||||
|
include(":blokk-server")
|
Reference in a new issue