Skip to main content

Writing your first empty server

To write your first Java Edition empty server you will need to write a basic TCP server.

caution

The project only supports the Java Edition currently.

Dependencies

These are the dependencies required to run the server.

api("me.gabrielleeg1:andesite-protocol-common:$andesite_version")
api("me.gabrielleeg1:andesite-protocol-java:$andesite_version")

implementation("io.ktor:ktor-network:$ktor_version")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_json_version")

implementation("net.benwoodworth.knbt:knbt:$knbt_version")

Getting Started

You will need to create a simple TCP server with Ktor.

main.kt
suspend fun main(): Unit = coroutineScope {
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind("0.0.0.0", 25565)

println("Listening connections at ${server.localAddress} for protocol ${codec.configuration.protocolVersion}")

while(true) {
val client = server.accept()

launch(CoroutineName("session-${client.remoteAddress}")) {
// TODO: handle the connection
}
}
}

And after that, you will instantiate the MinecraftCodec:

main.kt
val codec = MinecraftCodec {
protocolVersion = 756 // 756 is the sample version for the project, for 1.17.1;
nbt = Nbt { // Here you can configure the Nbt settings;
variant = NbtVariant.Java
compression = NbtCompression.None
ignoreUnknownKeys = true
}
json = Json { // and here, the json ones;
prettyPrint = true
}
serializersModule = SerializersModule { // here you can add new contextual serializers, etc...
contextual(UuidSerializer)
}
}

You will need a Session class to read and send packets, here is the snippet:

Session.kt
@OptIn(ExperimentalSerializationApi::class)
class Session(val codec: MinecraftCodec, val socket: Socket, val scope: CoroutineScope) : CoroutineScope by scope {
val input = socket.openReadChannel()
val output = socket.openWriteChannel()

suspend fun <T : JavaPacket> receivePacket(deserializer: DeserializationStrategy<T>): T {
val name = deserializer.descriptor.serialName

val size = input.readVarInt()
val packet = input.readPacket(size.toInt())

val id = packet.readVarInt().toInt()

println("Packet `$name` received with id [0x%02x] and size [$size]".format(id))

return codec.decodeFromByteArray(deserializer, packet.readBytes())
}

suspend fun <T : JavaPacket> sendPacket(serializer: SerializationStrategy<T>, packet: T) {
val packetName = serializer.descriptor.serialName
val packetId = extractPacketId(serializer.descriptor)

output.writePacket {
val data = buildPacket {
writeVarInt(packetId)
writeFully(codec.encodeToByteArray(serializer, packet))
}

println("Packet `$packetName` sent with id [0x%02x] with size [${data.remaining}]".format(packetId))

writeVarInt(data.remaining.toInt())
writePacket(data)
}

output.flush()
}
}

caution

This class is not production-ready. It is only a snippet to bootstrap the empty server.

Now with the Session class, you can handle the connections:

main.kt
val session = Session(codec, client, this)
val handshake = session.receivePacket(HandshakePacket.serializer())

when (handshake.nextState) {
NextState.Status -> {
session.sendPacket(
ResponsePacket.serializer(),
ResponsePacket(
Response(
version = Version(name = "Andesite for 1.17.1", protocol = 756),
players = Players(max = 10, online = 0, sample = listOf()),
description = Chat.of("&eAndesite for 1.17.1"), // The Chat API is a wrapper for Minecraft text components
),
),
)

session.receivePacket(PingPacket.serializer())
session.sendPacket(PongPacket.serializer(), PongPacket())
}

NextState.Login -> TODO("implement login logic")
}

The final source

The source code built with this tutorial is here