Variable byte
Event Mouse Move
Mouse movement packet is sent every client cycle when the user has done a mouse button press, or with a 40 client cycle interval if the user has only moved their mouse.
Table of contents
Payload
The packet is encoded in a loop, where it writes every recorded mouse click and movement since the last packet was sent. Do note that while the packet is not sent out every client cycle for movements, it does record movements each client cycle, and writes the differences. The packet can only fit a certain amount of recordings in it, causing another mouse move packet to be written right after if that count is exceeded, and so forth.
Header
The packet starts off by writing a header, which writes two fields which describe the time window between the very first recording of a mouse movement or click in this packet, and the very last one. The average time per iteration is multiplied by the number of recordings in this packet, and the excess time is then added to that sum, resulting in the total time in client cycles that the given packet covers.
Type | Description |
---|---|
Unsigned Byte | Average time per iteration, floored. |
Unsigned Byte | Excess time. |
Recordings
The rest of the packet that follows the header is then written in an indefinite loop, that continues to read recordings until there are no more bytes left in the buffer of the packet.
Each iteration starts off by checking(not reading) the first byte(in relation to the current position in the buffer). The packet reading then splits into four possible options:
Full Recording
If the last three bits(out of the last three) of the first byte in the loop are enabled, the large recording is read. This can be checked by the comparison of byteValue and 0xE0 == 0xE0
. This version is used by the client when there’s been more than 31 client cycles since the previous recording, and either the x or y offset in pixels is greater than what one byte can write(outside of range -128..127)
. Note that in the client, the x and y offsets in pixels are written as an int, however for the sake of simplicity, the two can be separated into individual short readings.
Type | Description |
---|---|
Unsigned Short | Time since last movement. Only the first 13 bits of the value are read, as the last three indicate the recording type. |
Short | X offset in pixels compared to the previous recording. |
Short | Y offset in pixels compared to the previous recording. |
Large Quick Recording
If the last two bits(out of the last three) of the first byte in the loop are enabled, the large quick recording is read. This can be checked by the comparison of byteValue and 0xE0 == 0xC0
. The large quick recording is written if the time since the previous recording is less than 32 client cycles, and either the x or y offset in pixels is greater than what one byte can write(outside of range -128..127)
. This is essentially all the same as full recording, except a smaller time window. Note that in the client, the x and y offsets in pixels are written as an int, however for the sake of simplicity, the two can be separated into individual short readings.
Type | Description |
---|---|
Unsigned Byte | Time since last movement. Only the first 5 bits of the value are read, as the last three indicate the recording type. |
Short | X offset in pixels compared to the previous recording. |
Short | Y offset in pixels compared to the previous recording. |
Medium Recording
If the last bit(out of the last three) of the first byte in the loop is enabled, the medium recording is read. This can be checked by the comparison of byteValue and 0xE0 == 0x80
. The medium recording is used by the client if the time since the previous recording is less than 32 client cycles, and both the x and y offset in pixels can fit within one byte each(inside of range -128..127)
. Note that in the client, the x and y offsets in pixels are written as a short, however for the sake of simplicity, the two can be separated into individual byte readings.
Type | Description |
---|---|
Unsigned Byte | Time since last movement. Only the first 5 bits of the value are read, as the last three indicate the recording type. |
Byte | X offset in pixels compared to the previous recording. |
Byte | Y offset in pixels compared to the previous recording. |
Small Recording
If none of the three last bits of the first byte in the loop are enabled, the small recording is read. This can be checked by the comparison of byteValue and 0xE0 == 0
. The small recording is used by the client if the time since the previous recording is less than 8 client cycles, and both the x and y offset in pixels can fit within 5 bits each(inside of range 0..31)
. In the client, all three of these values are bitpacked inside one unsigned short.
Type | Description |
---|---|
Unsigned Short | The first five bits indicate the y offset in pixels, the next five bits indicate the x offset in pixels, and the last three bits indicate the time since the last movement. |
Server Code Example
Below is an example of how to decode the packet from the server’s perspective:
override fun decode(player: Player, buffer: HeapByteBuffer, prot: ClientProt): EventMouseMoveEvent {
val averageTimePerIterationFloored = buffer.readUnsignedByte().toInt()
val excessTime = buffer.readUnsignedByte().toInt()
val entries = ArrayList<MouseMovement>(buffer.readableBytes() / 2)
while (buffer.isReadable) {
val currentByte = buffer.getByte(buffer.readerIndex()).toInt()
var xOffset: Int
var yOffset: Int
var timeSinceLastMovement: Int
when {
currentByte and 0xE0 == 0xE0 -> {
timeSinceLastMovement = buffer.readUnsignedShort() and 0x1FFF
yOffset = buffer.readShort().toInt()
xOffset = buffer.readShort().toInt()
if (xOffset == 0 && yOffset == -0x8000) {
xOffset = -1
yOffset = -1
}
}
currentByte and 0xE0 == 0xC0 -> {
timeSinceLastMovement = buffer.readUnsignedByte().toInt() and 0x1F
yOffset = buffer.readShort().toInt()
xOffset = buffer.readShort().toInt()
if (xOffset == 0 && yOffset == -0x8000) {
xOffset = -1
yOffset = -1
}
}
currentByte and 0xE0 == 0x80 -> {
timeSinceLastMovement = buffer.readUnsignedByte().toInt() and 0x1F
val packed = buffer.readUnsignedShort()
xOffset = (packed shr 8) - 128
yOffset = (packed and 0xFF) - 128
}
else -> {
val packed = buffer.readUnsignedShort()
timeSinceLastMovement = (packed shr 12) and 0x7
xOffset = (packed shr 6) and 0x1F
yOffset = packed and 0x1F
}
}
entries += MouseMovement(xOffset, yOffset, timeSinceLastMovement)
}
val timeWindow = (entries.size * averageTimePerIterationFloored) + excessTime
return EventMouseMoveEvent(timeWindow, entries)
}