Transformations
When you have defined packet, it might be changed in future versions of Minecraft. There is two ways to handle this:
- Define conditional packet transformation
- Define flat packet transformation
Conditional packet transformations
Section titled “Conditional packet transformations”Conditional packet transformations are easiest way to handle changes. However, they are likely to be tangled if packet changes are made very often. This may lead to a spaghetti code.
We will use previously defined Set Held Item (clientbound) packet as an example.
In this packet, changes were made from Minecraft version 1.21 to 1.21.2.
In version 1.21, the slot was defined as byte
, but in version 1.21.2 it was changed to varint
.
public class SetHeldItemClientboundPacket : IMinecraftClientboundPacket<SetHeldItemClientboundPacket>{ public required int Slot { get; set; }
public void Encode(ref MinecraftBuffer buffer, ProtocolVersion protocolVersion) { if (protocolVersion > ProtocolVersion.MINECRAFT_1_21) buffer.WriteVarInt(Slot); else buffer.WriteUnsignedByte((byte)Slot); }
public static SetHeldItemClientboundPacket Decode(ref MinecraftBuffer buffer, ProtocolVersion protocolVersion) { int slot;
if (protocolVersion > ProtocolVersion.MINECRAFT_1_21) slot = buffer.ReadVarInt(); else slot = buffer.ReadUnsignedByte();
return new SetHeldItemClientboundPacket { Slot = slot }; }
public void Dispose() { GC.SuppressFinalize(this); }}
Flat packet transformations
Section titled “Flat packet transformations”Flat packet transformations are more complex and harder to define, but they lead to more consistent behaviour and readable code. This idea was adopted from ViaVersion codebase.
When using these transformations, you have to keep only latest implementation of packet:
public class SetHeldItemClientboundPacket : IMinecraftClientboundPacket<SetHeldItemClientboundPacket>{ public required int Slot { get; set; }
public void Encode(ref MinecraftBuffer buffer, ProtocolVersion protocolVersion) { buffer.WriteVarInt(Slot); }
public static SetHeldItemClientboundPacket Decode(ref MinecraftBuffer buffer, ProtocolVersion protocolVersion) { return new SetHeldItemClientboundPacket { Slot = buffer.ReadVarInt() }; }
public void Dispose() { GC.SuppressFinalize(this); }}
Now when we have latest implementation, we have to define changes that were made throughout the versions.
In case of Set Held Item (clientbound) packet, just one change were made - slot
property type changed from byte
to varint
.
MinecraftPacketTransformationMapping[] Transformations { get; } = [ new(ProtocolVersion.MINECRAFT_1_21, ProtocolVersion.MINECRAFT_1_21_2, wrapper => { var slot = wrapper.Read<ByteProperty>(); wrapper.Write(new VarIntProperty(slot.Value)); }), new(ProtocolVersion.MINECRAFT_1_21_2, ProtocolVersion.MINECRAFT_1_21, wrapper => { var slot = wrapper.Read<VarIntProperty>(); wrapper.Write(new ByteProperty(slot.Value)); })];
Here we defined two transformations:
- From version 1.21 to 1.21.2 - we read
byte
property and write its value asvarint
property - upgrading our packet from previous version to next version. - From version 1.21.2 to 1.21 - we read
varint
property and write its value asbyte
property, vice versa - downgrading our packet from next version to previous version.
If your packet has unchanged properties, you can use wrapper.Passthrough()
method to skip them:
wrapper.Passthrough<ByteProperty>();
Register your flat transformations for player at same time, when you register your packet:
[Subscribe]public void OnPhaseChanged(PhaseChangedEvent @event){ if (@event.Phase is not Phase.Play) return;
@event.Player.RegisterPacket<SetHeldItemClientboundPacket>([ new(0x4F, ProtocolVersion.MINECRAFT_1_20_2), new(0x51, ProtocolVersion.MINECRAFT_1_20_3), new(0x53, ProtocolVersion.MINECRAFT_1_20_5), new(0x63, ProtocolVersion.MINECRAFT_1_21_2), new(0x62, ProtocolVersion.MINECRAFT_1_21_5) ]);
@event.Player.RegisterTransformations<SetHeldItemClientboundPacket>(Transformations);}
Now the Void proxy can use your defined transformations to transform packets to version that is used by the player. If player is playing on version 1.21, proxy will upgrade packet to your latest (1.21.2) version and pass it to the Events System. When plugin is sending packet to the player playing old version, proxy will downgrade packet to 1.21 and send it to the player.