It looks like the basic structure of the protocol is defined here. The document there looks like its laid out pretty close to the way you want it, actually, starting at the smallest protocol atoms and moving up to message types.
My recommended flow would be to go through this document, top to bottom, and start writing translations into methods on ByteBuffer. Typically this is how NIO protocols are built.
For example, I'd begin with the Data Types. Each of those should have an appropriate read and write method on ByteBuffer. Many of those will simply delegate to other ByteBuffer methods, but that's totally fine: your goal is that you want a bunch of methods that amount to saying "give me the next thing of this kind". An example here is in swift-nio-ssh, where a bunch of the core SSH data types got the appropriate methods.
Easy ones might be readMinecraftBoolean:
extension ByteBuffer {
mutating func readMinecraftBoolean() throws -> Bool? {
return try self.readInteger(as: UInt8.self).map {
switch $0 {
case 1: return true
case 0: return false
default: throw InvalidMinecraftBoolean($0)
}
}
}
More challenging ones might be readVarint, in part because you need to put the cursor back after you've done your parse:
extension ByteBuffer {
mutating func readMinecraftVarint() throws -> Int32? {
var copy = self
var shift = 0
var base = Int32(0)
while let nextByte = copy.readInteger(as: UInt8.self) {
value |= (nextByte & 0x7F) << shift
if nextByte & 0x80 != 0 {
// Complete parse, store the cursor and return.
self = copy
return value
}
position &+= 7
if position >= 32 { throw InvalidMinecraftVarint() }
}
// Ran out of bytes, return nil.
return nil
}
}
Once you have those, you can start writing message types. These should be structs that lay out the logical parts of a message. You can then write read/write extension methods on ByteBuffer that produce those things, by reading and writing their constituent elements. Again, swift-nio-ssh is a useful example.
With that done, you're almost ready to go. Define all the message types you understand in a big enum and then write a Parser type. This type accepts ByteBuffers and concatenates them together, and then has a nextMessage() method that will return the next message, assuming you have enough bytes.
You can wrap that Parser into a NIOByteToMessageDecoder, and then put that into a Channel, and you'll be able to read messages from the remote peer! You can use your write methods to test you got the encode correct, and you can then use them to build up a Serializer (which is a MessageToByteEncoder) to send messages the other way.