-
-
Notifications
You must be signed in to change notification settings - Fork 4
How packet (de ) serialization works
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
.
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).
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.
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.
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)?)
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")]