4 releases (2 breaking)

0.2.0 Dec 22, 2023
0.1.1 Sep 14, 2023
0.1.0 Sep 6, 2023
0.0.0 Aug 17, 2023

#14 in #ea


Used in tdf

MIT license

36KB
564 lines

Tdf

License Cargo Version Cargo Downloads

Library for serializing and deserializing the tdf network format used by BlazeSDK (Heat1) which is present in games such as Mass Effect 3, Mass Effect Andromeda, Battlefield 3, etc (Lots of EA games).

This is a re-write of my previous library blazepk. This version provides much more accurate naming and support for the different structures along with a derive macro implementation. This implementation also seperates out routing and packet framing which should be seperately as different versions use different framing implementations (Fire and Fire2)

Cargo

Using tdf with cargo

[dependencies]
tdf = "0.1"

or

cargo add tdf

Crate Features

The default features are ["serde", "derive"]

Feature Description
serde Adds support for serde serialization of tdf types
bytes Adds support for serializing directly onto BytesMut types from the bytes crate
derive Adds support for using tdf-derive derive macros for deriving structures and enums

Derive Macros

Tdf comes with the feature flag derive (Enabled by default) which allows deriving deserialize and serialize implementations automatically. There are 3 derive macros that you can use they are: TdfDeserialize, TdfSerialize, and TdfTyped

  • TdfDeserialize - Allows the structure to be deserialized from tdf bytes
  • TdfSerialize - Allows the structure to be serialized into tdf bytes
  • TdfTyped Derives the TdfType for the structure so that it can be used as a tag value
    • This is only supported for Groups, Repr Enums, and Tagged Enums

Deriving Structures

When creating structures for example to use as the contents of a Packet you should use this section, if you would like to use a structure as a group (Structures must be groups if you would like to tag them as values) see Deriving Groups for that.

Below is an example with a simple structure:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize)]
pub struct ExampleStructure {
    #[tdf(tag = "TEST")]
    pub my_value: u32
}

Lifetimes

You can provide a lifetime to a TdfDeserialize struct (The lifetime can be whatever you like and doesn't have to be 'de)

Lifetimes can be used for values like strings and Blobs to prevent needing to copy the underlying data, instead directly borrowing from the deserialize buffer:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize)]
pub struct ExampleStructure<'de> {
    #[tdf(tag = "TEST")]
    pub my_value: &'de str
}

Warning TdfDeserialize structures can NOT have more than one lifetime more than one lifetime will cause a compile time errror. However if you structure only implements TdfSerialize you can have as many lifetimes as you like

Generics

You can also provide generic parameters to your structures as long as the where clause is specified directly on the structure:

use tdf::prelude::*;

#[derive(TdfSerialize)]
pub struct ExampleStructure<T> 
where T: TdfSerialize + TdfTyped
{
    #[tdf(tag = "TEST")]
    pub my_value: T
}

Note Any generic types that you want to use as a tag must also implement the TdfTyped trait

When using generic types with TdfDeserialize you must specify the lifetime like in the lifetimes example, if your structure doesn't make use of the deserialize lifetime you will get an error for it being unused, you can solve this by using PhantomData and marking it as skip (See Skipping fields):

use tdf::prelude::*;
use std::marker::PhantomData;

#[derive(TdfSerialize, TdfDeserialize)]
pub struct ExampleStructure<'de, T> 
where T: TdfSerialize + TdfDeserialize<'de> + TdfTyped
{
    #[tdf(tag = "TEST")]
    pub my_value: T,
    #[tdf(skip)]
    pub _marker: PhantomData<&'de T>,
}

Skipping Fields

You can tell the macro to skip any fields that you don't want to include in serialization using #[tdf(skip)]

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize)]
pub struct ExampleStructure {
    #[tdf(tag = "TEST")]
    pub my_value: u32,
    #[tdf(skip)]
    pub skip_me: u32
}

Fields can also be skipped on tagged enum variants with named fields

Note If you are deriving TdfDeserialize any skipped types must implement Default because the deserializer will use the Default::default implementation for that type in order to fill in the struct

Deriving Groups

When serializing structures in order to represent them as a group value that can be tagged they must have special leading and trailing data which is created if you specify #[tdf(group)] on your structure:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize, TdfTyped)]
#[tdf(group)]
pub struct ExampleStructure {
    #[tdf(tag = "TEST")]
    pub my_value: u32
}

Once a structure is marked as a group you can derive the TdfTyped and use it as a group in tags

For structures that require a (2) prefix you can use the #[tdf(prefix_two)] attribute on the structure:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize, TdfTyped)]
#[tdf(group, prefix_two)]
pub struct ExampleStructure {
    #[tdf(tag = "TEST")]
    pub my_value: u32
}

Warning When a structure is marked as a group structure you cannot use it as a Packet body as it contains the extra bytes that represent it as a struct which will likely cause errors when another tool/client attempts to parse it

Repr Enums

If you want to define an enum of possible values that maps to one of the existing primitive types (u8, i8, u16, i16, u32, i32, u64, i64, usize, isize) you can define a repr enum:

Note Repr enums are serialized as variable-length integers aka TdfType::VarInt

Note Repr enums that implement TdfSerialize must also implement Clone + Copy

use tdf::prelude::*;

#[derive(Clone, Copy, TdfSerialize, TdfDeserialize, TdfTyped)]
#[repr(u8)]
pub enum ExampleReprEnum {
    Test = 0x0,
    Test2 = 0x1,
    Test3 = 0x3
}


Don't forget to derive TdfTyped to use this value within tags

Note When defining a repr enum you MUST use #[repr(%TYPE%)] otherwise the type will not be known

Warning Unlike standard repr enums you CANNOT omit the discriminant for each value, as at this stage the macro cannot infer the next values.

Default Variant

When using the above enum if a value you will run into a DecodeError at runtime if a discriminant not present in the enum is deserialized. To prevent this behavior you can specify a default variant:

use tdf::prelude::*;

#[derive(Clone, Copy, TdfSerialize, TdfDeserialize, TdfTyped)]
#[repr(u8)]
pub enum ExampleReprEnum {
    Test = 0x0,
    Test2 = 0x1,
    Test3 = 0x3,
    #[tdf(default)]
    MyDefault = 0x4,
}

Tagged Enums

You can represent BlazeSDK Tagged Unions as Rust enums with fields:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize, TdfTyped)]
pub enum ExampleTaggedEnum {
    #[tdf(key = 0x0, tag = "VALU")]
    Test {
        #[tdf(tag = "TEST")]
        test: String,
    },
    #[tdf(key = 0x1, tag = "VALU")]
    Test2(String)
}

In the above the "key" attribute for each enum variant is the discriminant to use for that variant. The "tag" attribute on each enum variant is the tag that will be used when tagging the value of the union

In the above there is a variant with named fields and one with a single unnamed field.

Variants with named fields are treated and serialized as groups (TdfType::Group) so the values within them have all the same conditions as Deriving Groups and most of the information from Deriving Structures also carries over.

Variants with unnamed fields (You are only allowed to specify 1 unnamed field as shown in ExampleTaggedEnum::Test2) the value is serialized as the type of value provided, in this case it Test2 would have a tagged value of VALU with the type of string.

Default Variant

When using the above enum just like in the repr example you will run into a DecodeError at runtime if a discriminant not present in the enum is deserialized. To prevent this behavior you can specify a default variant:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize, TdfTyped)]
pub enum ExampleTaggedEnum {
    #[tdf(key = 0x0, tag = "VALU")]
    Test {
        #[tdf(tag = "TEST")]
        test: String,
    },
    #[tdf(key = 0x1, tag = "VALU")]
    Test2(String),

    #[tdf(default)]
    DefaultValue
}

For tagged enums that implement TdfSerialize, if a default value is serialized it will use the unset discriminant

Unset Variant

Tdf tagged unions have a special unset type discriminant so you will run into a DecodeError at runtime if an unset discriminant is deserialized. To prevent this behavior you can specify a unset variant:

use tdf::prelude::*;

#[derive(TdfSerialize, TdfDeserialize, TdfTyped)]
pub enum ExampleTaggedEnum {
    #[tdf(key = 0x0, tag = "VALU")]
    Test {
        #[tdf(tag = "TEST")]
        test: String,
    },
    #[tdf(key = 0x1, tag = "VALU")]
    Test2(String),

    #[tdf(unset)]
    Unset
}

Note You cannot use a default variant as an unset variant they must be seperate as the default variant handles skipping extra values that might exist which will fail

Implementing TdfSerialize

Refer to docs.rs/tdf

Implementing TdfDeserialize

Refer to docs.rs/tdf

Dependencies

~0.6–1MB
~23K SLoC