Skip to content

How packet (de ) serialization works

Sergio edited this page Apr 10, 2024 · 4 revisions

Since v3.0.0 was changed the way of packet data (de-)serialization. This approach was introduced as "proc macro". So, the common way of using this approach is next:

#[derive(WorldPacket, Serialize, Deserialize)]
#[options(no_opcode)]
struct Income {
    skip: u32,
    message: String,
}

Usually each packet structure is used in separate handler. We attach WorldPacket trait to the struct which represents current packet. The struct should contain all fields the specific packet has. Also we need to attach Serialize, Deserialize traits (which are the part of serde crate), the traits are closely associated with WorldPacket trait. Optionally you can attach #[options()] attribute, which can contain next options: no_opcode and compressed.

Packet options

no_opcode is usually used when parsing income packet and opcode is not required to be known since we already know it from parent Processor that determine handlers list for specific opcode. Also we use this option when we have predefined packet body and opcode will be determined later (dynamically). compressed is used when income packet need to be decompressed (zlib).

Packet traits

For now there 2 packet traits available: LoginPacket and WorldPacket. Accordingly, LoginPacket is for using with LoginServer, WorldPacket - for WorldServer. Difference between packet types is in header structure.

Opcode

Although for income packet we usually do not need to set the opcode, we still need to set it for outcome packet (otherwise server cannot determine which packet it has been received). For this purpose with_opcode! macro can be used. The basic syntax is next:

with_opcode! {
    @world_opcode(Opcode::CMSG_NAME_QUERY)
    #[derive(WorldPacket, Serialize, Deserialize, Debug)]
    pub struct NameQueryOutcome {
        pub guid: u64,
    }
}

In the example above we wrap the packet struct into with_opcode! macro and then we attached @world_opcode() attribute with needed value as param. However, this way is not fit if we need to calculate opcode dynamically. In this case we can use unpack_with_opcode method:

Outcome {
	guid: PackedGuid(guid),
	unknown: 0
}.unpack_with_opcode(opcode)

This method can be called from struct instance.

Examples

Income

#[derive(WorldPacket, Serialize, Deserialize, Debug)]
#[options(no_opcode)]
struct Income {
    skip: u16,
    opcode: u16,
    target_guid: PackedGuid,
}

let (Income { opcode, target_guid, .. }, _) = Income::from_binary(
    input.data.as_ref().unwrap()
)?;

According to Rust syntax, we can attach each field we need to separate variable (in the example above we created opcode and target_guid variables). Income::from_binary is deserialization method, on input it expects raw packet data.

Outcome

#[derive(WorldPacket, Serialize, Deserialize, Debug)]
struct Outcome {
    #[serde(serialize_with = "crate::serializers::array_serializer::serialize_array")]
    unknown: [u8; 4],
}

To send outcome packet, it should be serialized first. To do this, you need to call .unpack() method from packet instance:

HandlerOutput::Data(Outcome {
	unknown: [0xFF, 0xFF, 0xFF, 0xFF]
}.unpack_with_opcode(Opcode::CMSG_REALM_SPLIT)?)

(De-) Serialization of fixed-length array

To do this action, you should apply next attribute to each field with fixed-length array:

#[serde(serialize_with = "crate::serializers::array_serializer::serialize_array")]