View Source Packet Definitions

In this section, we will learn how to use ElvenGard.Network.PacketSerializer.

We'll use the previously created types to transform our network protocol into Elixir structures.

macros

Macros

defpacket-macro

defpacket Macro

First, we'll take a look at how the ElvenGard.Network.PacketSerializer.defpacket/3 macro works.

Here is the most basic example for a packet definition that you can have:

# Textual packet
defpacket "HEADER", as: TextualPacket

# Binary packet
defpacket 0x0000, as: BinaryPacket

This will generate a simple structure, without any field.

The first parameter of the defpacket is its ID. It's used to identify it, so that we know which fields to decode next.

We also have to specify a :as option. This is the name of the generated structure. For example, the following code will generate a structure called MyApp.MyPacket with the binary packet header 0x0000.

defmodule MyApp do
  defpacket 0x0000, as: MyPacket
end

Guards

Now, let's imagine you have several packets with the same packet header and to find out how to deserialize it, you need to access a socket-related state. To do this, you can use socket assigns and add a guard to the packet:

defpacket 0x0000 when socket.assigns.state == :init, as: InitPacket
defpacket 0x0000 when socket.assigns.state == :sync, as: SyncPacket

In this example, if when deserializing the packet, the socket state is :init, the structure returned by our deserializer will be InitPacket and if it is :sync, it will be SyncPacket.

For more information and examples of how to use guards, I recommend you read the code in examples/minecraft_ex.

NOTE: guards are only available for deserialization (client packets).

Body

Finally, to define fields in our packet, their name, type etc., we'll use a do ... end block.

defpacket 0x0000, as: MyPacket do
  field ...
end

field-macro

field Macro

Now that you know how to create a packet, it's time to see how to create fields. To do this, you can use the field macro.

It's very easy to use:

field :field1, FieldType
field :field2, FieldType, opt: :value

Here, the first line will add a field to our packet with field1 as name and FieldType as type. The name must be an atom and the field type a module which use the ElvenGard.Network.Type behaviour. The third parameter is optional and is a keyword list defining options to send when types will be decoded (see ElvenGard.Network.Type.encode/2 and ElvenGard.Network.Type.decode/2).

decorators

Decorators

Using defpacket and field, you can now create structures with different fields, but you still can't serialize or deserialize them in order to send them to or receive them from the client.

ElvenGard.Network defined 2 decorators for this:

  • @serializable true: indicates that the packet is intended to be serialized. It is therefore a server packet.
  • @deserializable true: indicates that the packet is intended to be deserialized. It is therefore a client packet.

By tagging a packet as deserializable, the defpacket macro will automatically create a deserialize/1 function inside the generated module.

Tagging a packet as deserializable/3 will, this time, create 2 deserialize/3 functions, one inside the generated module and one outside it. The latter will redirect to the former.

These helper functions will come in very handy in our Network Codec, as we'll see in the next chapter.

Now that we know the theory, it's time to put it into practice.

client-packets

Client Packets

Let's first define our client packets.

# file: lib/login_server/client_packets.ex
defmodule LoginServer.ClientPackets do
  @moduledoc """
  Documentation for LoginServer.ClientPackets
  """

  use ElvenGard.Network.PacketSerializer

  alias LoginServer.Types.StringType

  ## Ping packet

  @deserializable true
  defpacket "PING", as: PingRequest

  ## Login packet

  @deserializable true
  defpacket "LOGIN", as: LoginRequest do
    field :username, StringType
    field :password, StringType
  end
end

These packets are pretty straightforward, so there's not much to explain. Just note the presence of @deserializable true, which clearly indicates that these packets are intended to be deserialized as they are received from the client.

server-packets

Server Packets

Now let's define the server's one.

# file: lib/login_server/server_packets.ex
defmodule LoginServer.ServerPackets do
  @moduledoc """
  Documentation for LoginServer.ServerPackets
  """

  use ElvenGard.Network.PacketSerializer

  alias LoginServer.Types.{DateTimeType, StringType}
  alias LoginServer.SubPackets.WorldInfo

  ## Ping packet

  @serializable true
  defpacket "PONG", as: PongResponse do
    field :time, DateTimeType
  end

  ## Login packets

  @serializable true
  defpacket "FAIL", as: LoginFailed do
    field :reason, StringType
  end

  @serializable true
  defpacket "SUCCESS", as: LoginSucceed do
    field :world, WorldInfo, sep: ":"
  end
end

Once again, these packets are pretty straightforward. We use @serializable true to indicate that we will send them to the client.

Moreover, we're using the WorldInfo sub-packet which, remember, expects the field separator as an option. This is where we define it.

summary

Summary

You now know how to create a packet that can be serialized and deserialized. Now it's time to encode or decode it so that it can transit over the network.