From 6eb9b29b789f9f54f75c41988d5d5b104ae62437 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 26 Jul 2023 01:07:23 +0200 Subject: [PATCH 01/59] :memo: Update README --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ab1eace..168ef7d 100644 --- a/README.md +++ b/README.md @@ -5,49 +5,49 @@ ## What is ElvenGard -Currently, all independent developers wishing to create a MMORPG type game have already asked themselves the question of how to make the server part easily while being "solid" (minimum crash, latency, ...). +Currently, all independent developers wishing to create a Multiplayer game have already asked themselves the question of how to make the server part easily while being "solid" (minimum crash, latency, ...). Indeed, for the client part, there are many tools very well designed to realize it (Unity3D, Unreal Engine,...) but for the server part, each game being different there are currently very few solutions to do this work. This is the goal of this project: make a toolkit to group together different functionalities present in any MMORPG (network part, quests, movements, objects in game, instances, etc...) to prepare bases for the developer so that he doesn't have to dwell on this part which is often tedious. -## Who is this project for ? +## What is ElvenGard.Network -Initially, this project is intended for anyone who wants to create a game without having to re-code the server part from scratch. -It is also intended for people who want to create an emulator for an existing game. For my tests, I use Nostale as an example. So I already have a predefined network protocol and I don't have to code a client. Later, I also plan to test it with World of Warcraft and FlyFF. Since the network protocols of these games are totally different, this will allow me to test the abstraction of the different features of this toolkit. +[ElvenGard.Network](https://github.com/ImNotAVirus/elvengard_network) is a dedicated toolkit designed to streamline and enhance the network aspect of game server development. Built on top of the "ranch" Erlang library, this powerful toolkit provides developers with a robust foundation for creating multiplayer game servers with ease. -## Installation +Key Features: -Currently not [available in Hex](https://hex.pm/docs/publish), you can use it like that: +1. **Specialized Network Handling:** ElvenGard.Network is solely focused on handling the complexities of networking in multiplayer game servers. It offers a high-performance solution optimized for managing player connections, communication, and data exchange. -```elixir -def deps do - [ - {:elvengard_network, github: "imnotavirus/elvengard_network"} - ] -end -``` +2. **Efficient Protocol Management:** The toolkit simplifies the implementation of network protocols, allowing developers to define and manage packet structures efficiently. This streamlined approach ensures smooth and reliable communication between the game client and server. + +3. **Packet Processing Made Easy:** ElvenGard.Network streamlines packet processing, making it straightforward to handle incoming and outgoing data. Developers can effortlessly manage packet reception, interpretation, and response, thereby reducing the burden of low-level network management. + +4. **Custom Serialization/Deserialization:** With support for custom data types, ElvenGard.Network enables seamless serialization and deserialization of data. This feature ensures compatibility and efficient data exchange between different components of the game server. + +5. **Minimized Latency and Crashes:** By leveraging the "ranch" library, ElvenGard.Network benefits from Erlang's reliability and scalability. This helps minimize latency issues and reduces the risk of crashes, providing a stable networking foundation for multiplayer games. + +With ElvenGard.Network handling the intricate network tasks, game developers can focus on creating captivating gameplay, dynamic worlds, and engaging player experiences. By leveraging this specialized toolkit, the journey from conceptualizing a multiplayer game to deploying a robust server becomes significantly smoother and more efficient. + +## Installation -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `elvengard_network` to your list of dependencies in `mix.exs`: +The package can be installed by adding `elvengard_network` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:elvengard_network, "~> 0.1.0"} + {:elvengard_network, "~> 0.1"} ] end ``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at [https://hexdocs.pm/elvengard_network](https://hexdocs.pm/elvengard_network). +The docs can be found at [https://hexdocs.pm/elvengard_network](https://hexdocs.pm/elvengard_network). **/!\ This application is currently not production ready !** ## Projects using ElvenGard -- [Flügel](https://github.com/ImNotAVirus/Flugel-NostaleEmu): Created by the same developer as ElvenGard, this is a [Nostale](http://nostale.com/) emulator -- [Imanity](https://github.com/ImNotAVirus/Imanity-FlyffEmu): Created by the same developer as ElvenGard, this is a [FlyFF](http://flyff.webzen.com) emulator +- [MinecraftEx](https://github.com/ImNotAVirus/elvengard_network/tree/master/examples/minecraft_ex): Located in the `examples` folder of the repository, this is the beginning of a [Minecraft](https://www.minecraft.net) server emulator +- [AvantHeim](https://github.com/ImNotAVirus/AvantHeim): Created by the same developer as ElvenGard, this is a [NosTale](https://gameforge.com/en-US/play/nostale) server emulator ## Contributing From fa651f42a43c38264370f01b1ec662ca86822b65 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:17:00 +0200 Subject: [PATCH 02/59] :memo: Add module doc for `ElvenGard.Network.PacketSchema` --- lib/elven_gard/network/packet_schema.ex | 74 ++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_schema.ex index 3367e42..26cbfbd 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_schema.ex @@ -1,6 +1,78 @@ defmodule ElvenGard.Network.PacketSchema do @moduledoc ~S""" - ElvenGard.Network.PacketSchema + Packet Schema DSL for defining received packet structures. + + This module provides a DSL (Domain-Specific Language) for defining packet schemas + for received packets in a network protocol. It enables users to create structured + packets with specified fields and decode binary data into packet structs. + + To learn more about the available macros and how to define packet schemas, refer + to the [Packet Schema DSL documentation](). + + ## Packets Macros + + The `packet` macros (`packet/1`, `packet/2` and `packet/3`) allow users to define + packet structures. + They always require a packet ID. The alias (which is the name of the generated + packet structure), the guard (with the `when` keyword), and do-block (for + defining fields) are optional. + + Users can specify guards in packets macros to conditionally match packets based + on a condition (often using socket assigns). + + For more details on using the packets macros, please refer to the guide at + or to examples. + + ## Packet Structure and Decoding + + The `packets` macros generate both a packet structure and a `decode/3` function + for each defined packet. The packet structure represents the decoded packet data, + and the `decode/3` function takes binary data and converts it into the structure + based on the defined schema. + + The `decode/3` function generated for each packet should be used by + `c:ElvenGard.Network.PacketCodec.deserialize/2`. + + ## Field Macros + + The field macros are used to define fields within a packet: + + - `field/2`: Define a field with a name and type. + - `field/3`: Define a field with a name, type, and decoding options. + + ## Examples + + defmodule MyApp.Endpoint.PacketSchemas do + use ElvenGard.Network.PacketSchema + + alias MyApp.Types.{Integer, String} + + # Simple string packet: + # - This generate a SimplePacket structure + # - The `decode/3` function will match all packet with `simple_packet` as packet id + # - The structure will have 2 fields: `id` and `name` (a string and an integer) + packet "simple_packet" do + field :id, Integer + field :name, String + end + + # Complex string packet: + # - This generate a AliasedModule structure + # - The `decode/3` function will match packet with `complex_packet` as packet id ONLY if the state is `:init` + # - The structure will have 2 fields: `id` and `name` (a string and an integer) + # - We're also passing `[fill: true]` as option to our `MyApp.Types.String.decode/2` + packet "complex_packet" when socket.assigns.state == :init, as: AliasedModule do + field :id, Integer + field :name, String, fill: true + end + + # Simple binary packet + # - This generate a KeepAlivePacket structure + # - The `decode/3` function will match packet with `0x0000` as packet id + # - The structure will have no field + # - Here the `:as` option is required because the packet ID is an integer + packet 0x0000, as: KeepAlivePacket + """ @type packet_id :: integer() | binary() From 2149272be7dc36d841899188e606f1dffe2ad166 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:32:43 +0200 Subject: [PATCH 03/59] :fire: Remove unused type --- lib/elven_gard/network/packet_schema.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_schema.ex index 26cbfbd..05ee91a 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_schema.ex @@ -75,8 +75,6 @@ defmodule ElvenGard.Network.PacketSchema do """ - @type packet_id :: integer() | binary() - ## Helpers defguardp is_packet_id(id) when is_integer(id) or is_binary(id) From 10eca72408c895f43eac8e902e216c8a18bd6efb Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:36:42 +0200 Subject: [PATCH 04/59] :recycle: Move PacketCodex to the root folder --- .../minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex | 2 +- lib/elven_gard/network/{endpoint => }/packet_codec.ex | 2 +- lib/elven_gard/network/socket.ex | 2 +- test/support/dummy_encoder.ex | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename lib/elven_gard/network/{endpoint => }/packet_codec.ex (91%) diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex index cec5e37..b645e1e 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex @@ -3,7 +3,7 @@ defmodule MinecraftEx.Endpoint.PacketCodec do Documentation for MinecraftEx.Endpoint.PacketCodec """ - @behaviour ElvenGard.Network.Endpoint.PacketCodec + @behaviour ElvenGard.Network.PacketCodec alias MinecraftEx.Types.VarInt alias MinecraftEx.Endpoint.PacketSchemas diff --git a/lib/elven_gard/network/endpoint/packet_codec.ex b/lib/elven_gard/network/packet_codec.ex similarity index 91% rename from lib/elven_gard/network/endpoint/packet_codec.ex rename to lib/elven_gard/network/packet_codec.ex index 99e43e3..e80da1a 100644 --- a/lib/elven_gard/network/endpoint/packet_codec.ex +++ b/lib/elven_gard/network/packet_codec.ex @@ -1,4 +1,4 @@ -defmodule ElvenGard.Network.Endpoint.PacketCodec do +defmodule ElvenGard.Network.PacketCodec do @moduledoc ~S""" Define a behaviour for packet encoding and decoding """ diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index 181565a..5dfcedc 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -9,7 +9,7 @@ defmodule ElvenGard.Network.Socket do * `:transport` - A [Ranch transport](https://ninenines.eu/docs/en/ranch/2.0/guide/transports/) * `:transport_pid` - The pid of the socket's transport process * `:remaining` - The remaining bytes after a receive and a packet deserialization - * `:encoder` - The `ElvenGard.Network.Endpoint.PacketCodec` used to encode packets in `send/2` function + * `:encoder` - The `ElvenGard.Network.PacketCodec` used to encode packets in `send/2` function """ alias __MODULE__ diff --git a/test/support/dummy_encoder.ex b/test/support/dummy_encoder.ex index 65f68f7..1b8dabb 100644 --- a/test/support/dummy_encoder.ex +++ b/test/support/dummy_encoder.ex @@ -1,9 +1,9 @@ defmodule ElvenGard.Network.DummyEncoder do @moduledoc false - @behaviour ElvenGard.Network.Endpoint.PacketCodec - @impl true + @behaviour ElvenGard.Network.PacketCodec + @impl true def next(_raw), do: raise("unimplemented") @impl true From 176d3d4df4b91d0c2bdfae156983dd142a812da7 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 26 Jul 2023 23:50:26 +0200 Subject: [PATCH 05/59] :memo: Write `ElvenGard.Network.Type` doc --- lib/elven_gard/network/type.ex | 69 ++++++++++++++++++++++++++++++++-- lib/elven_gard/network/uuid.ex | 2 + 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/lib/elven_gard/network/type.ex b/lib/elven_gard/network/type.ex index 59148e5..786daf8 100644 --- a/lib/elven_gard/network/type.ex +++ b/lib/elven_gard/network/type.ex @@ -1,12 +1,75 @@ defmodule ElvenGard.Network.Type do @moduledoc ~S""" - Define a behaviour for custom types (packet parsing) + Define a behaviour for custom types (packet parsing). + + This module defines a behaviour that allows users to create custom packet parsing + types. By implementing the callbacks defined in this module, users can define + how a specific field type is decoded and encoded when parsing and generating + packets. + + To implement a custom type, you need to define the `decode/2` and `encode/2` + callbacks. The `decode/2` function takes a raw binary as input and decodes it + into a structured value of the custom type. The `encode/2` function takes a value + of the custom type and encodes it into a binary representation. + + Note that the `ElvenGard.Network.PacketSchema` module uses the callbacks in this + behaviour when decoding packet fields. For each field defined in a packet schema, + the corresponding type's `decode/2` function will be called to parse the binary + data. + + ## Example + + Here's an example of defining a custom type for decoding and encoding a 16-bit + little-endian integer: + + defmodule MyLittleEndianIntType do + use ElvenGard.Network.Type + + @impl ElvenGard.Network.Type + def decode(raw, _opts) do + <> = raw + {value, rest} + end + + @impl ElvenGard.Network.Type + def encode(value, _opts) do + <> + end + end + + In the above example, we defined a custom type module `MyLittleEndianIntType` that + decodes and encodes a 16-bit little-endian integer. + + Then, when defining a packet schema using `ElvenGard.Network.PacketSchema`, you + can use this custom type to define a field in the packet structure. + """ - @doc "Decode a field type" + ## Callbacks + + @doc """ + Decode a raw bitstring into a structured value of the custom type. + + Arguments: + + - `raw`: The raw bitstring to be decoded. + - `opts`: A keyword list of options that can be used during decoding (optional). + + The function should return a tuple with the decoded value and the remaining + unparsed bitstring. + """ @callback decode(raw :: bitstring, opts :: keyword) :: {any, remaining :: bitstring} - @doc "Encode a field type" + @doc """ + Encode a value of the custom type into binary data. + + Arguments: + + - `value`: The value of the custom type to be encoded. + - `opts`: A keyword list of options that can be used during encoding (optional). + + The function should return the encoded binary data as an iodata. + """ @callback encode(value :: any, opts :: keyword) :: iodata defmacro __using__(_env) do diff --git a/lib/elven_gard/network/uuid.ex b/lib/elven_gard/network/uuid.ex index 09edca8..b8d25a7 100644 --- a/lib/elven_gard/network/uuid.ex +++ b/lib/elven_gard/network/uuid.ex @@ -44,6 +44,8 @@ defmodule ElvenGard.Network.UUID do >> end + ## Private functions + @compile {:inline, e: 1} @doc false From acdf7256b6f85e7aa615e700a04ee39bc49d23b6 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Thu, 27 Jul 2023 22:08:43 +0200 Subject: [PATCH 06/59] :memo: Add documentation for `ElvenGard.Network.View` --- lib/elven_gard/network/view.ex | 40 ++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/elven_gard/network/view.ex b/lib/elven_gard/network/view.ex index f2d3e5f..f72ddc6 100644 --- a/lib/elven_gard/network/view.ex +++ b/lib/elven_gard/network/view.ex @@ -1,12 +1,48 @@ defmodule ElvenGard.Network.View do @moduledoc ~S""" - Define a custom behaviour for a view (packet sent from server to client) + Define a custom behaviour for a View (packet sent from server to client). + + This module defines a behaviour that allows users to create custom packet views + to be sent from the server to the client. By implementing the callback defined + in this module, users can define how a specific packet view is rendered and + converted into binary data for transmission. + + To implement a custom view, you need to define the `render/2` callback. The + `render/2` function takes the name of the packet view and a map of parameters + as input and returns the binary data to be sent to the client. + + ## Example + + Here's an example of defining a custom view for rendering a login response + packet: + + defmodule MyLoginResponseView do + use ElvenGard.Network.View + + @impl ElvenGard.Network.View + def render(:login_response, %{status: status, message: message}) do + <> + end + end + + In the above example, we defined a custom view module `MyLoginResponseView` that + implements the `render/2` callback for rendering a login response packet. + + Then, when sending a login response packet using `ElvenGard.Network.PacketView`, you + can use this custom view to render the packet before sending it to the client. """ alias ElvenGard.Network.UnknownViewError @doc """ - Build a packet to send to the client + Build a packet to send to the client. + + Arguments: + + - `name`: A unique identifier packet view (string or atom). + - `params`: A map or keyword list of parameters to be used when rendering the view. + + The function should return an iodata to be sent to the client. """ @callback render(name, params) :: iodata() when name: atom() | String.t(), params: map() | Keyword.t() From 18779849be6d0ca238814debb27af03dfcb591da Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Thu, 27 Jul 2023 23:42:20 +0200 Subject: [PATCH 07/59] :memo: Add documentation for `ElvenGard.Network.Socket` --- lib/elven_gard/network/socket.ex | 38 +++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index 5dfcedc..13dff54 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -1,15 +1,24 @@ defmodule ElvenGard.Network.Socket do @moduledoc ~S""" - Manage a socket + Manage a socket. + + This module provides functionality for managing a socket in the network protocol. + A socket is a connection between the server and a client. It maintains various + socket fields, such as the socket ID, socket assigns, transport information, + and the packet encoder used for sending data. ## Socket fields - * `:id` - The string id of the socket - * `:assigns` - The map of socket assigns, default: `%{}` - * `:transport` - A [Ranch transport](https://ninenines.eu/docs/en/ranch/2.0/guide/transports/) - * `:transport_pid` - The pid of the socket's transport process - * `:remaining` - The remaining bytes after a receive and a packet deserialization - * `:encoder` - The `ElvenGard.Network.PacketCodec` used to encode packets in `send/2` function + - `:id`: The unique string ID of the socket. + - `:assigns`: A map of socket assigns, which can be used to store custom data + associated with the socket. The default value is `%{}`. + - `:transport`: The [Ranch transport](https://ninenines.eu/docs/en/ranch/2.0/guide/transports/) + used for the socket. + - `:transport_pid`: The PID (Process ID) of the socket's transport process. + - `:remaining`: The remaining bytes after receiving and packet deserialization. + - `:encoder`: The `ElvenGard.Network.PacketCodec` module used to encode packets + in the `send/2` function. + """ alias __MODULE__ @@ -32,7 +41,10 @@ defmodule ElvenGard.Network.Socket do } @doc """ - Create a new structure + Create a new socket structure. + + This function initializes a new socket with the given `transport_pid`, `transport`, + and `encoder` module. """ @spec new(pid, atom, module) :: Socket.t() def new(transport_pid, transport, encoder) do @@ -46,6 +58,16 @@ defmodule ElvenGard.Network.Socket do @doc """ Send a packet to the client. + + This function sends a packet to the client through the socket's transport. + If the socket's `encoder` is set to `:unset`, the data is sent as is. + Otherwise, the `encoder` module is used to serialize the data before sending it. + + ## Examples + + iex> ElvenGard.Network.Socket.send(socket, {:login_response, %{status: 200, message: "Welcome!"}}) + :ok + """ @spec send(Socket.t(), any) :: :ok | {:error, atom} def send(%Socket{encoder: :unset} = socket, data) do From 4e147ca9ff333a745cfe4b8533aa3de8e11a20e1 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Thu, 27 Jul 2023 23:56:09 +0200 Subject: [PATCH 08/59] :recycle: Rename packet codec into network codec --- examples/echo_server/config/config.exs | 2 +- examples/minecraft_ex/config/config.exs | 2 +- .../endpoint/{packet_codec.ex => network_codec.ex} | 6 +++--- lib/elven_gard/network/endpoint/protocol.ex | 2 +- .../network/{packet_codec.ex => network_codec.ex} | 2 +- lib/elven_gard/network/packet_schema.ex | 2 +- lib/elven_gard/network/socket.ex | 2 +- test/support/dummy_encoder.ex | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) rename examples/minecraft_ex/lib/minecraft_ex/endpoint/{packet_codec.ex => network_codec.ex} (86%) rename lib/elven_gard/network/{packet_codec.ex => network_codec.ex} (92%) diff --git a/examples/echo_server/config/config.exs b/examples/echo_server/config/config.exs index 264dd24..d201068 100644 --- a/examples/echo_server/config/config.exs +++ b/examples/echo_server/config/config.exs @@ -18,4 +18,4 @@ config :echo_server, EchoServer.Endpoint.Protocol, packet_handler: :unset, # Here we are not using packet coder/decoder. # We send raw packets - packet_codec: :unset + network_codec: :unset diff --git a/examples/minecraft_ex/config/config.exs b/examples/minecraft_ex/config/config.exs index 65fb598..0237bc3 100644 --- a/examples/minecraft_ex/config/config.exs +++ b/examples/minecraft_ex/config/config.exs @@ -14,4 +14,4 @@ config :minecraft_ex, MinecraftEx.Endpoint, config :minecraft_ex, MinecraftEx.Endpoint.Protocol, packet_handler: MinecraftEx.Endpoint.PacketHandlers, - packet_codec: MinecraftEx.Endpoint.PacketCodec + network_codec: MinecraftEx.Endpoint.NetworkCodec diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex similarity index 86% rename from examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex rename to examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex index b645e1e..6627727 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex @@ -1,9 +1,9 @@ -defmodule MinecraftEx.Endpoint.PacketCodec do +defmodule MinecraftEx.Endpoint.NetworkCodec do @moduledoc """ - Documentation for MinecraftEx.Endpoint.PacketCodec + Documentation for MinecraftEx.Endpoint.NetworkCodec """ - @behaviour ElvenGard.Network.PacketCodec + @behaviour ElvenGard.Network.NetworkCodec alias MinecraftEx.Types.VarInt alias MinecraftEx.Endpoint.PacketSchemas diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index 8fe99f2..d4a3c64 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -115,7 +115,7 @@ defmodule ElvenGard.Network.Endpoint.Protocol do @app Mix.Project.get().project[:app] defp env_config(), do: Application.fetch_env!(@app, __MODULE__) - defp codec(), do: env_config()[:packet_codec] + defp codec(), do: env_config()[:network_codec] defp handlers(), do: env_config()[:packet_handler] defp packet_loop(data, socket) do diff --git a/lib/elven_gard/network/packet_codec.ex b/lib/elven_gard/network/network_codec.ex similarity index 92% rename from lib/elven_gard/network/packet_codec.ex rename to lib/elven_gard/network/network_codec.ex index e80da1a..58afa67 100644 --- a/lib/elven_gard/network/packet_codec.ex +++ b/lib/elven_gard/network/network_codec.ex @@ -1,4 +1,4 @@ -defmodule ElvenGard.Network.PacketCodec do +defmodule ElvenGard.Network.NetworkCodec do @moduledoc ~S""" Define a behaviour for packet encoding and decoding """ diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_schema.ex index 05ee91a..fcf341e 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_schema.ex @@ -31,7 +31,7 @@ defmodule ElvenGard.Network.PacketSchema do based on the defined schema. The `decode/3` function generated for each packet should be used by - `c:ElvenGard.Network.PacketCodec.deserialize/2`. + `c:ElvenGard.Network.NetworkCodec.deserialize/2`. ## Field Macros diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index 13dff54..5ccf928 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -16,7 +16,7 @@ defmodule ElvenGard.Network.Socket do used for the socket. - `:transport_pid`: The PID (Process ID) of the socket's transport process. - `:remaining`: The remaining bytes after receiving and packet deserialization. - - `:encoder`: The `ElvenGard.Network.PacketCodec` module used to encode packets + - `:encoder`: The `ElvenGard.Network.NetworkCodec` module used to encode packets in the `send/2` function. """ diff --git a/test/support/dummy_encoder.ex b/test/support/dummy_encoder.ex index 1b8dabb..827ab4d 100644 --- a/test/support/dummy_encoder.ex +++ b/test/support/dummy_encoder.ex @@ -1,7 +1,7 @@ defmodule ElvenGard.Network.DummyEncoder do @moduledoc false - @behaviour ElvenGard.Network.PacketCodec + @behaviour ElvenGard.Network.NetworkCodec @impl true def next(_raw), do: raise("unimplemented") From 5ebf924f5a543221016b0e19bb4316cc025a1493 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Fri, 28 Jul 2023 23:47:14 +0200 Subject: [PATCH 09/59] :recycle: Refacto PacketSchema --- lib/elven_gard/network/packet_schema.ex | 110 +++++++++++++----------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_schema.ex index fcf341e..72b34bc 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_schema.ex @@ -145,31 +145,25 @@ defmodule ElvenGard.Network.PacketSchema do end defmacro __before_compile__(env) do - egn_packets = Module.get_attribute(env.module, :egn_packets) - - egn_packets - |> Enum.map(fn %{id: id, name: name, fields: fields, guards: guards} = packet -> - quote location: :keep, generated: true do - defmodule :"#{Module.concat(__MODULE__, unquote(name))}" do - # Structure - - defstruct Enum.map(unquote(Macro.escape(fields)), & &1.name) - - # Introspection - - @doc false - def __schema__(:id), do: unquote(id) - def __schema__(:name), do: unquote(name) - def __schema__(:guards), do: unquote(Macro.escape(guards)) - def __schema__(:fields), do: unquote(Macro.escape(fields)) - end + decode_body_fun = fn %{id: id, name: name} -> + quote do + :"#{Module.concat(__MODULE__, unquote(name))}".decode( + unquote(id), + var!(data), + var!(socket) + ) + end + end - # Decode term to struct + egn_packets = Module.get_attribute(env.module, :egn_packets) + packet_structures = Enum.map(egn_packets, &def_structures/1) + decode_functions = Enum.map(egn_packets, &def_decode(&1, decode_body_fun)) - unquote(decode_ast(packet)) - end - end) - |> Kernel.++([schemas_introspection()]) + quote do + unquote(schemas_introspection()) + unquote_splicing(packet_structures) + unquote_splicing(decode_functions) + end end ## Private funtions @@ -180,6 +174,13 @@ defmodule ElvenGard.Network.PacketSchema do raise "a module name is required for integer packet ids" end + defp schemas_introspection() do + quote do + @doc false + def __schemas__(), do: @egn_packets + end + end + defp do_packet(id, name, guards, exp) do quote location: :keep do Module.delete_attribute(__MODULE__, :egn_packet_fields) @@ -205,23 +206,24 @@ defmodule ElvenGard.Network.PacketSchema do end end - defp schemas_introspection() do - quote do - @doc false - def __schemas__(), do: @egn_packets - end - end - - defp decode_ast(packet) do - %{id: id, name: name, fields: fields, guards: guards} = packet + defp def_decode(%{id: id, guards: guards} = packet, body_cb) do guards = if is_nil(guards), do: true, else: guards - fields_ast = fields |> Enum.map(&field_ast(&1, &1[:opts][:if])) |> merge_ast_blocks() quote location: :keep, generated: true do def decode(var!(packet_id) = unquote(id), var!(data), var!(socket)) when unquote(guards) do - var!(packet) = struct(Module.concat(__MODULE__, unquote(name)), packet_id: unquote(id)) + unquote(body_cb.(packet)) + end + end + end - unquote(fields_ast) + defp def_structures(%{id: id, name: name, guards: guards, fields: fields} = packet) do + fields_ast = Enum.map(fields, &decode_field_ast(&1, &1[:opts][:if])) + + decode_body_fun = fn %{name: name} -> + quote location: :keep do + var!(packet) = %__MODULE__{} + + unquote_splicing(fields_ast) if var!(data) != "" do iname = inspect(unquote(name)) @@ -231,17 +233,37 @@ defmodule ElvenGard.Network.PacketSchema do var!(packet) end end + + quote location: :keep do + defmodule :"#{Module.concat(__MODULE__, unquote(name))}" do + # Structure + + defstruct Enum.map(unquote(Macro.escape(fields)), & &1.name) + + # Introspection + + @doc false + def __schema__(:id), do: unquote(id) + def __schema__(:name), do: unquote(name) + def __schema__(:guards), do: unquote(Macro.escape(guards)) + def __schema__(:fields), do: unquote(Macro.escape(fields)) + + # Encoder/Decoder + + unquote(def_decode(packet, decode_body_fun)) + end + end end - defp field_ast(%{name: name, type: type, opts: opts}, nil) do - quote location: :keep, generated: true do + defp decode_field_ast(%{name: name, type: type, opts: opts}, nil) do + quote location: :keep do {value, var!(data)} = unquote(type).decode(var!(data), unquote(opts)) var!(packet) = Map.put(var!(packet), unquote(name), value) end end - defp field_ast(%{name: name, type: type, opts: opts}, condition) do - quote location: :keep, generated: true do + defp decode_field_ast(%{name: name, type: type, opts: opts}, condition) do + quote location: :keep do {value, var!(data)} = case unquote(condition) do result when result in [false, nil] -> {nil, var!(data)} @@ -251,14 +273,4 @@ defmodule ElvenGard.Network.PacketSchema do var!(packet) = Map.put(var!(packet), unquote(name), value) end end - - defp merge_ast_blocks(blocks) do - blocks - |> Macro.prewalk(fn - {:__block__, _, block} -> block - ast -> ast - end) - |> List.flatten() - |> then(&{:__block__, [], &1}) - end end From 2bdc95cea54fe791342bfea724a324f83ce02ba0 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Fri, 28 Jul 2023 23:59:10 +0200 Subject: [PATCH 10/59] :pencil2: Typo --- lib/elven_gard/network/packet_schema.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_schema.ex index 72b34bc..7a2198e 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_schema.ex @@ -156,7 +156,7 @@ defmodule ElvenGard.Network.PacketSchema do end egn_packets = Module.get_attribute(env.module, :egn_packets) - packet_structures = Enum.map(egn_packets, &def_structures/1) + packet_structures = Enum.map(egn_packets, &def_structure/1) decode_functions = Enum.map(egn_packets, &def_decode(&1, decode_body_fun)) quote do @@ -216,7 +216,7 @@ defmodule ElvenGard.Network.PacketSchema do end end - defp def_structures(%{id: id, name: name, guards: guards, fields: fields} = packet) do + defp def_structure(%{id: id, name: name, guards: guards, fields: fields} = packet) do fields_ast = Enum.map(fields, &decode_field_ast(&1, &1[:opts][:if])) decode_body_fun = fn %{name: name} -> From e515175291683fc216354133fb69e52a38f8ed72 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sat, 29 Jul 2023 02:10:23 +0200 Subject: [PATCH 11/59] :recycle: Add encode function --- lib/elven_gard/network/packet_schema.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_schema.ex index 7a2198e..6fbef5a 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_schema.ex @@ -206,6 +206,15 @@ defmodule ElvenGard.Network.PacketSchema do end end + defp def_encode(%{fields: fields}) do + quote location: :keep, generated: true do + def encode(%__MODULE__{} = var!(packet), var!(socket), var!(opts) \\ []) do + unquote(Macro.escape(fields)) + |> Enum.map(& &1.type.encode(var!(packet)[&1.name])) + end + end + end + defp def_decode(%{id: id, guards: guards} = packet, body_cb) do guards = if is_nil(guards), do: true, else: guards @@ -250,6 +259,7 @@ defmodule ElvenGard.Network.PacketSchema do # Encoder/Decoder + unquote(def_encode(packet)) unquote(def_decode(packet, decode_body_fun)) end end From 7ceea5e51b5099f5d27bf47a34cb755028340019 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 8 Aug 2023 00:31:59 +0200 Subject: [PATCH 12/59] :construction: WIP improvements --- .formatter.exs | 3 +- examples/minecraft_ex/lib/minecraft_ex.ex | 2 + .../lib/minecraft_ex/client_packets.ex | 10 ++ .../handshake_packets.ex} | 24 ++-- .../client_packets/login_packets.ex | 21 +++ .../minecraft_ex/endpoint/network_codec.ex | 4 +- .../minecraft_ex/endpoint/packet_handlers.ex | 2 +- .../lib/minecraft_ex/endpoint/packet_views.ex | 11 +- .../server_packets/handshake_packets.ex | 23 ++++ ...{packet_schema.ex => packet_serializer.ex} | 121 ++++++++++++++---- lib/elven_gard/network/type.ex | 6 +- test.exs | 66 ++++++++++ .../elven_gard/network/packet_schema_test.exs | 8 +- 13 files changed, 240 insertions(+), 61 deletions(-) create mode 100644 examples/minecraft_ex/lib/minecraft_ex/client_packets.ex rename examples/minecraft_ex/lib/minecraft_ex/{endpoint/packet_schemas.ex => client_packets/handshake_packets.ex} (59%) create mode 100644 examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex create mode 100644 examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex rename lib/elven_gard/network/{packet_schema.ex => packet_serializer.ex} (73%) create mode 100644 test.exs diff --git a/.formatter.exs b/.formatter.exs index f381a71..f0c8040 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -11,7 +11,8 @@ locals_without_parens = [ field: 3, packet: 1, packet: 2, - packet: 3 + packet: 3, + import_packets: 1 ] [ diff --git a/examples/minecraft_ex/lib/minecraft_ex.ex b/examples/minecraft_ex/lib/minecraft_ex.ex index 5072ab8..2abe38f 100644 --- a/examples/minecraft_ex/lib/minecraft_ex.ex +++ b/examples/minecraft_ex/lib/minecraft_ex.ex @@ -2,4 +2,6 @@ defmodule MinecraftEx do @moduledoc """ Documentation for `MinecraftEx`. """ + + defguard has_state(socket, state) when socket.assigns.state == state end diff --git a/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex new file mode 100644 index 0000000..1eb8973 --- /dev/null +++ b/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex @@ -0,0 +1,10 @@ +defmodule MinecraftEx.ClientPackets do + @moduledoc """ + TODO: MinecraftEx.ClientPackets + """ + + import ElvenGard.Network.PacketSerializer, only: [import_packets: 1] + + import_packets MinecraftEx.Client.HandshakePackets + import_packets MinecraftEx.Client.LoginPackets +end diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_schemas.ex b/examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex similarity index 59% rename from examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_schemas.ex rename to examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex index c3a0dfc..c665791 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_schemas.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex @@ -1,25 +1,24 @@ -defmodule MinecraftEx.Endpoint.PacketSchemas do +defmodule MinecraftEx.Client.HandshakePackets do @moduledoc """ - Documentation for MinecraftEx.Endpoint.PacketSchemas + Documentation for MinecraftEx.Client.HandshakePackets """ - use ElvenGard.Network.PacketSchema + use ElvenGard.Network.PacketSerializer + + import MinecraftEx, only: [has_state: 2] alias MinecraftEx.Types.{ - Boolean, Enum, Long, MCString, Short, - UUID, VarInt } - defguardp has_state(socket, state) when socket.assigns.state == state - ## Handshake packets # 0x00 Handshake - state=init + @deserializable true packet 0x00 when has_state(socket, :init), as: Handshake do field :protocol_version, VarInt field :server_address, MCString @@ -28,19 +27,12 @@ defmodule MinecraftEx.Endpoint.PacketSchemas do end # 0x00 Status Request - state=status + @deserializable true packet 0x00 when has_state(socket, :status), as: StatusRequest # 0x01 Ping Request - state=status + @deserializable true packet 0x01 when has_state(socket, :status), as: PingRequest do field :payload, Long, sign: :signed end - - ## Login packets - - # 0x00 Login Start - state=login - packet 0x00 when has_state(socket, :login), as: LoginStart do - field :name, MCString - field :player_uuid?, Boolean - field :player_uuid, UUID, if: packet.player_uuid? - end end diff --git a/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex new file mode 100644 index 0000000..d784a09 --- /dev/null +++ b/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex @@ -0,0 +1,21 @@ +defmodule MinecraftEx.Client.LoginPackets do + @moduledoc """ + Documentation for MinecraftEx.Client.LoginPackets + """ + + use ElvenGard.Network.PacketSerializer + + import MinecraftEx, only: [has_state: 2] + + alias MinecraftEx.Types.{Boolean, MCString, UUID} + + ## Login packets + + # 0x00 Login Start - state=login + @deserializable true + packet 0x00 when has_state(socket, :login), as: LoginStart do + field :name, MCString + field :player_uuid?, Boolean + field :player_uuid, UUID, if: packet.player_uuid? + end +end diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex index 6627727..a442e58 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex @@ -6,7 +6,7 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do @behaviour ElvenGard.Network.NetworkCodec alias MinecraftEx.Types.VarInt - alias MinecraftEx.Endpoint.PacketSchemas + alias MinecraftEx.ClientPackets @impl true def next(<<>>), do: {nil, <<>>} @@ -27,7 +27,7 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do @impl true def deserialize(raw, socket) do {packet_id, rest} = VarInt.decode(raw) - packet = PacketSchemas.decode(packet_id, rest, socket) + packet = ClientPackets.decode(packet_id, rest, socket) if is_nil(packet) do raise "unable to decode packet with id #{inspect(packet_id)} - #{inspect(raw)}" diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex index d505aa6..b586d18 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex @@ -8,7 +8,7 @@ defmodule MinecraftEx.Endpoint.PacketHandlers do alias ElvenGard.Network.Socket alias MinecraftEx.Resources - alias MinecraftEx.Endpoint.PacketSchemas.{ + alias MinecraftEx.Client.HandshakePackets.{ Handshake, PingRequest, StatusRequest diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex index 761f3f8..b232a0f 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex @@ -5,19 +5,14 @@ defmodule MinecraftEx.Endpoint.PacketViews do use ElvenGard.Network.View - alias MinecraftEx.Types.{Long, MCString} + alias MinecraftEx.Server.HandshakePackets.{PongResponse, StatusResponse} @impl true def render(:status_response, %{status: status}) do - json = Poison.encode!(status) - packet(0x00, [MCString.encode(json)]) + %StatusResponse{json: Poison.encode!(status)} end def render(:pong_response, %{payload: payload}) do - packet(0x01, [Long.encode(payload)]) + %PongResponse{payload: payload} end - - ## Helpers - - defp packet(id, params), do: [<> | params] end diff --git a/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex new file mode 100644 index 0000000..e9188ba --- /dev/null +++ b/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex @@ -0,0 +1,23 @@ +defmodule MinecraftEx.Server.HandshakePackets do + @moduledoc """ + Documentation for MinecraftEx.Server.HandshakePackets + """ + + use ElvenGard.Network.PacketSerializer + + alias MinecraftEx.Types.{Long, MCString} + + ## Handshake packets + + # 0x00 Handshake + @serializable true + packet 0x00, as: StatusResponse do + field :json, MCString + end + + # 0x00 Handshake + @serializable true + packet 0x01, as: PongResponse do + field :payload, Long + end +end diff --git a/lib/elven_gard/network/packet_schema.ex b/lib/elven_gard/network/packet_serializer.ex similarity index 73% rename from lib/elven_gard/network/packet_schema.ex rename to lib/elven_gard/network/packet_serializer.ex index 6fbef5a..7dfd293 100644 --- a/lib/elven_gard/network/packet_schema.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -1,13 +1,13 @@ -defmodule ElvenGard.Network.PacketSchema do +defmodule ElvenGard.Network.PacketSerializer do @moduledoc ~S""" - Packet Schema DSL for defining received packet structures. + Packet Serializer DSL for defining received packet structures. - This module provides a DSL (Domain-Specific Language) for defining packet schemas + This module provides a DSL (Domain-Specific Language) for defining packet serializers for received packets in a network protocol. It enables users to create structured packets with specified fields and decode binary data into packet structs. - To learn more about the available macros and how to define packet schemas, refer - to the [Packet Schema DSL documentation](). + To learn more about the available macros and how to define packet serializers, refer + to the [Packet Serializer DSL documentation](). ## Packets Macros @@ -21,7 +21,7 @@ defmodule ElvenGard.Network.PacketSchema do on a condition (often using socket assigns). For more details on using the packets macros, please refer to the guide at - or to examples. + or to examples. ## Packet Structure and Decoding @@ -42,8 +42,8 @@ defmodule ElvenGard.Network.PacketSchema do ## Examples - defmodule MyApp.Endpoint.PacketSchemas do - use ElvenGard.Network.PacketSchema + defmodule MyApp.Endpoint.PacketSerializers do + use ElvenGard.Network.PacketSerializer alias MyApp.Types.{Integer, String} @@ -88,7 +88,7 @@ defmodule ElvenGard.Network.PacketSchema do # packet 0x0000 when ... defmacro packet({:when, _, [id, guards]}) when is_packet_id(id) do - do_packet(id, id_to_name(id), guards, nil) + do_packet(id, id_to_name(id), expand_aliases(guards, __CALLER__), nil) end # packet 0x0000, as: ModuleName @@ -98,27 +98,37 @@ defmodule ElvenGard.Network.PacketSchema do # packet 0x0000 when ..., as: ModuleName defmacro packet({:when, _, [id, guards]}, as: name) when is_packet_id(id) do - do_packet(id, name, guards, nil) + guards + |> Macro.expand(__CALLER__) + |> Macro.to_string() + |> IO.puts() + + do_packet(id, name, expand_aliases(guards, __CALLER__), nil) end # packet 0x0000 do ... end defmacro packet(id, do: exp) when is_packet_id(id) do - do_packet(id, id_to_name(id), nil, exp) + do_packet(id, id_to_name(id), nil, expand_aliases(exp, __CALLER__)) end # packet 0x0000 when ... do ... end defmacro packet({:when, _, [id, guards]}, do: exp) when is_packet_id(id) do - do_packet(id, id_to_name(id), guards, exp) + do_packet( + id, + id_to_name(id), + expand_aliases(guards, __CALLER__), + expand_aliases(exp, __CALLER__) + ) end # packet 0x0000, as: ModuleName do ... end defmacro packet(id, [as: name], do: exp) when is_packet_id(id) do - do_packet(id, name, nil, exp) + do_packet(id, name, nil, expand_aliases(exp, __CALLER__)) end # packet 0x0000 when ..., as: ModuleName do ... end defmacro packet({:when, _, [id, guards]}, [as: name], do: exp) when is_packet_id(id) do - do_packet(id, name, guards, exp) + do_packet(id, name, expand_aliases(guards, __CALLER__), expand_aliases(exp, __CALLER__)) end # field :protocol_version, VarInt @@ -126,6 +136,23 @@ defmodule ElvenGard.Network.PacketSchema do do_field(name, type, opts) end + defmacro import_packets(mod) do + # decode_body_fun = fn %{mod: mod} -> + # quote location: :keep do + # unquote(mod).decode(var!(packet_id), var!(data), var!(socket)) + # end + # end + + mod = expand_aliases(mod, __CALLER__) + # decode_ast = Enum.map(mod.__schemas__(), &def_decode(&1, decode_body_fun)) + + mod.module_info |> IO.inspect() + + quote location: :keep do + # (unquote_splicing(decode_ast)) + end + end + defmacro __using__(_env) do quote location: :keep do import unquote(__MODULE__), @@ -138,34 +165,39 @@ defmodule ElvenGard.Network.PacketSchema do ] @before_compile unquote(__MODULE__) + @after_compile unquote(__MODULE__) Module.register_attribute(__MODULE__, :egn_packet_fields, accumulate: true) Module.register_attribute(__MODULE__, :egn_packets, accumulate: true) + + @serializable false + @deserializable false end end defmacro __before_compile__(env) do - decode_body_fun = fn %{id: id, name: name} -> + decode_body_fun = fn %{id: id, mod: mod} -> quote do - :"#{Module.concat(__MODULE__, unquote(name))}".decode( - unquote(id), - var!(data), - var!(socket) - ) + unquote(mod).decode(unquote(id), var!(data), var!(socket)) end end egn_packets = Module.get_attribute(env.module, :egn_packets) - packet_structures = Enum.map(egn_packets, &def_structure/1) decode_functions = Enum.map(egn_packets, &def_decode(&1, decode_body_fun)) quote do unquote(schemas_introspection()) - unquote_splicing(packet_structures) unquote_splicing(decode_functions) end end + defmacro __after_compile__(env, _bytecode) do + env.module + |> Module.get_attribute(:egn_packets) + |> Enum.map(&def_structure/1) + |> Code.compile_quoted() + end + ## Private funtions defp id_to_name(id) when is_binary(id), do: :"Elixir.#{Macro.camelize(id)}" @@ -174,6 +206,13 @@ defmodule ElvenGard.Network.PacketSchema do raise "a module name is required for integer packet ids" end + defp expand_aliases(ast, env) do + Macro.postwalk(ast, fn + {:__aliases__, _, _} = alias_ast -> Macro.expand(alias_ast, env) + ast -> ast + end) + end + defp schemas_introspection() do quote do @doc false @@ -183,6 +222,11 @@ defmodule ElvenGard.Network.PacketSchema do defp do_packet(id, name, guards, exp) do quote location: :keep do + if not @serializable and not @deserializable do + mod = Module.concat(__MODULE__, unquote(name)) + IO.warn("packet #{inspect(mod)} defined but not serializable nor deserializable") + end + Module.delete_attribute(__MODULE__, :egn_packet_fields) unquote(exp) @@ -190,9 +234,16 @@ defmodule ElvenGard.Network.PacketSchema do @egn_packets %{ id: unquote(id), name: unquote(name), + parent: __MODULE__, + mod: Module.concat(__MODULE__, unquote(name)), guards: unquote(Macro.escape(guards)), - fields: Enum.reverse(@egn_packet_fields) + fields: Enum.reverse(@egn_packet_fields), + serializable: @serializable, + deserializable: @deserializable } + + @serializable false + @deserializable false end end @@ -225,7 +276,18 @@ defmodule ElvenGard.Network.PacketSchema do end end - defp def_structure(%{id: id, name: name, guards: guards, fields: fields} = packet) do + defp def_structure(packet) do + %{ + id: id, + name: name, + parent: parent, + mod: mod, + guards: guards, + fields: fields, + serializable: serializable, + deserializable: deserializable + } = packet + fields_ast = Enum.map(fields, &decode_field_ast(&1, &1[:opts][:if])) decode_body_fun = fn %{name: name} -> @@ -244,7 +306,9 @@ defmodule ElvenGard.Network.PacketSchema do end quote location: :keep do - defmodule :"#{Module.concat(__MODULE__, unquote(name))}" do + defmodule unquote(mod) do + import unquote(parent), except: [decode: 3, encode: 3] + # Structure defstruct Enum.map(unquote(Macro.escape(fields)), & &1.name) @@ -259,7 +323,12 @@ defmodule ElvenGard.Network.PacketSchema do # Encoder/Decoder - unquote(def_encode(packet)) + # if unquote(serializable) do + # unquote(def_encode(packet)) + # else + + # end + unquote(def_decode(packet, decode_body_fun)) end end diff --git a/lib/elven_gard/network/type.ex b/lib/elven_gard/network/type.ex index 786daf8..dbddfcf 100644 --- a/lib/elven_gard/network/type.ex +++ b/lib/elven_gard/network/type.ex @@ -12,8 +12,8 @@ defmodule ElvenGard.Network.Type do into a structured value of the custom type. The `encode/2` function takes a value of the custom type and encodes it into a binary representation. - Note that the `ElvenGard.Network.PacketSchema` module uses the callbacks in this - behaviour when decoding packet fields. For each field defined in a packet schema, + Note that the `ElvenGard.Network.PacketSerializer` module uses the callbacks in this + behaviour when decoding packet fields. For each field defined in a packet serializer, the corresponding type's `decode/2` function will be called to parse the binary data. @@ -40,7 +40,7 @@ defmodule ElvenGard.Network.Type do In the above example, we defined a custom type module `MyLittleEndianIntType` that decodes and encodes a 16-bit little-endian integer. - Then, when defining a packet schema using `ElvenGard.Network.PacketSchema`, you + Then, when defining a packet serializer using `ElvenGard.Network.PacketSerializer`, you can use this custom type to define a field in the packet structure. """ diff --git a/test.exs b/test.exs new file mode 100644 index 0000000..d3fd4eb --- /dev/null +++ b/test.exs @@ -0,0 +1,66 @@ +defmodule DSL do + defmacro deffunction({:when, _, [name, guards]} = a) do + # # IO.inspect(__CALLER__, label: "__CALLER__") + # IO.inspect(guards, label: "guards") + # IO.inspect(function_exported?(__CALLER__.module, :__info__, 1)) + + # IO.inspect(:elixir_utils.extract_guards(a), label: "AAAA") + # IO.inspect(Macro.decompose_call(guards), label: "BBBB") + + # IO.inspect(:elixir_locals.cache_env(__CALLER__), label: "CCCC") + # # :elixir_def.take_definition() + + env = Map.put(__CALLER__, :context, :env) + + guards + |> IO.inspect() + |> Macro.expand_once(env) + # |> Macro.prewalk(&Macro.expand(&1, __CALLER__)) + # |> :elixir_expand.expand(:elixir_env.env_to_ex(__CALLER__), __CALLER__) + |> Macro.to_string() + |> IO.puts() + + # |> IO.inspect() + + quote do + defmodule CustomModule do + def unquote(name)(var!(arg1)) when unquote(guards) do + IO.inspect(:ok) + end + end + end + end +end + +defmodule MyApp2 do + defguard is_true(value) when value == "1" + + defmacro test2(arg1) do + quote do + arg1 == 123 + end + end +end + +defmodule MyApp3 do + defmacro test3 do + quote do + 3 + end + end +end + +defmodule MyApp do + import DSL, only: [deffunction: 1] + import MyApp2 + require MyApp3 + + defguard is_true(value) when value == "1" + + deffunction :my_function when is_true(arg1) + # deffunction :my_function2 when arg1.test == 3 + # deffunction :my_function3 when test2(arg1) + # deffunction :my_function4 when MyApp3.test3 == 3 +end + +MyApp.CustomModule.my_function("1") diff --git a/test/elven_gard/network/packet_schema_test.exs b/test/elven_gard/network/packet_schema_test.exs index 18c2131..2682963 100644 --- a/test/elven_gard/network/packet_schema_test.exs +++ b/test/elven_gard/network/packet_schema_test.exs @@ -1,8 +1,8 @@ -defmodule ElvenGard.Network.PacketSchemaTest do +defmodule ElvenGard.Network.PacketSerializerTest do use ExUnit.Case, async: true defmodule SimplePackets do - use ElvenGard.Network.PacketSchema + use ElvenGard.Network.PacketSerializer use ExUnit.Case, async: true alias ElvenGard.Network.CustomTypes.{Boolean, Date, Int, Str} @@ -79,7 +79,7 @@ defmodule ElvenGard.Network.PacketSchemaTest do end defmodule StringPackets do - use ElvenGard.Network.PacketSchema + use ElvenGard.Network.PacketSerializer use ExUnit.Case, async: true alias ElvenGard.Network.CustomTypes.{Boolean, Str} @@ -208,7 +208,7 @@ defmodule ElvenGard.Network.PacketSchemaTest do assert_raise RuntimeError, ~r/a module name is required for integer packet ids/, fn -> Code.compile_string(""" defmodule CantCompile do - use ElvenGard.Network.PacketSchema + use ElvenGard.Network.PacketSerializer packet 0x00, do: :ok end """) From 2a8d3d31392694c32f01397d7a926b11162619b8 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 8 Aug 2023 15:56:36 +0200 Subject: [PATCH 13/59] :ambulance: Fix guards expansion --- examples/minecraft_ex/mix.exs | 2 +- lib/elven_gard/network/packet_serializer.ex | 48 +++++++-------- test.exs | 66 --------------------- 3 files changed, 21 insertions(+), 95 deletions(-) delete mode 100644 test.exs diff --git a/examples/minecraft_ex/mix.exs b/examples/minecraft_ex/mix.exs index 6305cf6..d88a13c 100644 --- a/examples/minecraft_ex/mix.exs +++ b/examples/minecraft_ex/mix.exs @@ -5,7 +5,7 @@ defmodule MinecraftEx.MixProject do [ app: :minecraft_ex, version: "0.1.0", - elixir: "~> 1.14", + elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps() ] diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 7dfd293..4f55fff 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -83,52 +83,42 @@ defmodule ElvenGard.Network.PacketSerializer do # packet 0x0000 defmacro packet(id) when is_packet_id(id) do - do_packet(id, id_to_name(id), nil, nil) + do_packet(id, id_to_name(id), nil, nil, __CALLER__) end # packet 0x0000 when ... defmacro packet({:when, _, [id, guards]}) when is_packet_id(id) do - do_packet(id, id_to_name(id), expand_aliases(guards, __CALLER__), nil) + do_packet(id, id_to_name(id), guards, nil, __CALLER__) end # packet 0x0000, as: ModuleName defmacro packet(id, as: name) when is_packet_id(id) do - do_packet(id, name, nil, nil) + do_packet(id, name, nil, nil, __CALLER__) end # packet 0x0000 when ..., as: ModuleName defmacro packet({:when, _, [id, guards]}, as: name) when is_packet_id(id) do - guards - |> Macro.expand(__CALLER__) - |> Macro.to_string() - |> IO.puts() - - do_packet(id, name, expand_aliases(guards, __CALLER__), nil) + do_packet(id, name, guards, nil, __CALLER__) end # packet 0x0000 do ... end defmacro packet(id, do: exp) when is_packet_id(id) do - do_packet(id, id_to_name(id), nil, expand_aliases(exp, __CALLER__)) + do_packet(id, id_to_name(id), nil, exp, __CALLER__) end # packet 0x0000 when ... do ... end defmacro packet({:when, _, [id, guards]}, do: exp) when is_packet_id(id) do - do_packet( - id, - id_to_name(id), - expand_aliases(guards, __CALLER__), - expand_aliases(exp, __CALLER__) - ) + do_packet(id, id_to_name(id), guards, exp, __CALLER__) end # packet 0x0000, as: ModuleName do ... end defmacro packet(id, [as: name], do: exp) when is_packet_id(id) do - do_packet(id, name, nil, expand_aliases(exp, __CALLER__)) + do_packet(id, name, nil, exp, __CALLER__) end # packet 0x0000 when ..., as: ModuleName do ... end defmacro packet({:when, _, [id, guards]}, [as: name], do: exp) when is_packet_id(id) do - do_packet(id, name, expand_aliases(guards, __CALLER__), expand_aliases(exp, __CALLER__)) + do_packet(id, name, guards, exp, __CALLER__) end # field :protocol_version, VarInt @@ -137,19 +127,17 @@ defmodule ElvenGard.Network.PacketSerializer do end defmacro import_packets(mod) do - # decode_body_fun = fn %{mod: mod} -> - # quote location: :keep do - # unquote(mod).decode(var!(packet_id), var!(data), var!(socket)) - # end - # end + decode_body_fun = fn %{mod: mod} -> + quote location: :keep do + unquote(mod).decode(var!(packet_id), var!(data), var!(socket)) + end + end mod = expand_aliases(mod, __CALLER__) - # decode_ast = Enum.map(mod.__schemas__(), &def_decode(&1, decode_body_fun)) - - mod.module_info |> IO.inspect() + decode_ast = Enum.map(mod.__schemas__(), &def_decode(&1, decode_body_fun)) quote location: :keep do - # (unquote_splicing(decode_ast)) + (unquote_splicing(decode_ast)) end end @@ -220,7 +208,11 @@ defmodule ElvenGard.Network.PacketSerializer do end end - defp do_packet(id, name, guards, exp) do + defp do_packet(id, name, guards, exp, caller) do + guard_env = %{caller | context: :guard} + guards = Macro.postwalk(guards, &Macro.expand(&1, guard_env)) + exp = expand_aliases(exp, caller) + quote location: :keep do if not @serializable and not @deserializable do mod = Module.concat(__MODULE__, unquote(name)) diff --git a/test.exs b/test.exs deleted file mode 100644 index d3fd4eb..0000000 --- a/test.exs +++ /dev/null @@ -1,66 +0,0 @@ -defmodule DSL do - defmacro deffunction({:when, _, [name, guards]} = a) do - # # IO.inspect(__CALLER__, label: "__CALLER__") - # IO.inspect(guards, label: "guards") - # IO.inspect(function_exported?(__CALLER__.module, :__info__, 1)) - - # IO.inspect(:elixir_utils.extract_guards(a), label: "AAAA") - # IO.inspect(Macro.decompose_call(guards), label: "BBBB") - - # IO.inspect(:elixir_locals.cache_env(__CALLER__), label: "CCCC") - # # :elixir_def.take_definition() - - env = Map.put(__CALLER__, :context, :env) - - guards - |> IO.inspect() - |> Macro.expand_once(env) - # |> Macro.prewalk(&Macro.expand(&1, __CALLER__)) - # |> :elixir_expand.expand(:elixir_env.env_to_ex(__CALLER__), __CALLER__) - |> Macro.to_string() - |> IO.puts() - - # |> IO.inspect() - - quote do - defmodule CustomModule do - def unquote(name)(var!(arg1)) when unquote(guards) do - IO.inspect(:ok) - end - end - end - end -end - -defmodule MyApp2 do - defguard is_true(value) when value == "1" - - defmacro test2(arg1) do - quote do - arg1 == 123 - end - end -end - -defmodule MyApp3 do - defmacro test3 do - quote do - 3 - end - end -end - -defmodule MyApp do - import DSL, only: [deffunction: 1] - import MyApp2 - require MyApp3 - - defguard is_true(value) when value == "1" - - deffunction :my_function when is_true(arg1) - # deffunction :my_function2 when arg1.test == 3 - # deffunction :my_function3 when test2(arg1) - # deffunction :my_function4 when MyApp3.test3 == 3 -end - -MyApp.CustomModule.my_function("1") From 848b7e4cd05068ddd26f5a214220e634999c5719 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:11:01 +0200 Subject: [PATCH 14/59] :sparkles: Conditionally add encode callback to packets --- lib/elven_gard/network/packet_serializer.ex | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 4f55fff..09dcc6e 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -315,13 +315,17 @@ defmodule ElvenGard.Network.PacketSerializer do # Encoder/Decoder - # if unquote(serializable) do - # unquote(def_encode(packet)) - # else - - # end + if unquote(serializable) do + unquote(def_encode(packet)) + else + def encode(_, _, _ \\ []), do: raise("unimplemented") + end - unquote(def_decode(packet, decode_body_fun)) + if unquote(deserializable) do + unquote(def_decode(packet, decode_body_fun)) + else + def decode(_, _, _), do: raise("unimplemented") + end end end end From bd6e0d13e1f376d80305be964e5ca1c54e6d0ddc Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 8 Aug 2023 23:31:31 +0200 Subject: [PATCH 15/59] :ambulance: Fix example serialization --- .../minecraft_ex/endpoint/network_codec.ex | 12 ++++++++---- lib/elven_gard/network/packet_serializer.ex | 19 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex index a442e58..a2d57e1 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex @@ -37,10 +37,14 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do end @impl true - def serialize(raw, _socket) do - packet_length = - raw |> List.wrap() |> Enum.map(&byte_size/1) |> Enum.sum() |> VarInt.encode([]) + def serialize(struct, socket) when is_struct(struct) do + {packet_id, params} = struct.__struct__.encode(struct) + serialize([VarInt.encode(packet_id), params], socket) + end - [<> | raw] + def serialize(raw, _socket) when is_list(raw) do + bin = :binary.list_to_bin(raw) + packet_length = bin |> byte_size() |> VarInt.encode([]) + [<> | bin] end end diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 09dcc6e..6511504 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -249,11 +249,20 @@ defmodule ElvenGard.Network.PacketSerializer do end end - defp def_encode(%{fields: fields}) do + defp def_encode(%{id: id, fields: fields}) do + fields_ast = + Enum.map(fields, fn %{name: name, type: type, opts: opts} -> + quote location: :keep do + unquote(type).encode( + Map.fetch!(var!(packet), unquote(name)), + unquote(opts) + ) + end + end) + quote location: :keep, generated: true do - def encode(%__MODULE__{} = var!(packet), var!(socket), var!(opts) \\ []) do - unquote(Macro.escape(fields)) - |> Enum.map(& &1.type.encode(var!(packet)[&1.name])) + def encode(%__MODULE__{} = var!(packet)) do + {unquote(id), unquote(fields_ast)} end end end @@ -318,7 +327,7 @@ defmodule ElvenGard.Network.PacketSerializer do if unquote(serializable) do unquote(def_encode(packet)) else - def encode(_, _, _ \\ []), do: raise("unimplemented") + def encode(_), do: raise("unimplemented") end if unquote(deserializable) do From 238581616575c8927de692e4fee33925f1bad2f0 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:07:39 +0200 Subject: [PATCH 16/59] :recycle: Rename `packet` macro into `defpacket` --- .formatter.exs | 6 +-- .../client_packets/handshake_packets.ex | 8 ++-- .../client_packets/login_packets.ex | 2 +- .../server_packets/handshake_packets.ex | 4 +- lib/elven_gard/network/packet_serializer.ex | 48 +++++++++---------- .../elven_gard/network/packet_schema_test.exs | 44 ++++++++++++----- 6 files changed, 65 insertions(+), 47 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index f0c8040..3561e81 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -9,9 +9,9 @@ inputs = locals_without_parens = [ field: 2, field: 3, - packet: 1, - packet: 2, - packet: 3, + defpacket: 1, + defpacket: 2, + defpacket: 3, import_packets: 1 ] diff --git a/examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex index c665791..9c6f564 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/client_packets/handshake_packets.ex @@ -4,7 +4,7 @@ defmodule MinecraftEx.Client.HandshakePackets do """ use ElvenGard.Network.PacketSerializer - + import MinecraftEx, only: [has_state: 2] alias MinecraftEx.Types.{ @@ -19,7 +19,7 @@ defmodule MinecraftEx.Client.HandshakePackets do # 0x00 Handshake - state=init @deserializable true - packet 0x00 when has_state(socket, :init), as: Handshake do + defpacket 0x00 when has_state(socket, :init), as: Handshake do field :protocol_version, VarInt field :server_address, MCString field :server_port, Short, sign: :unsigned @@ -28,11 +28,11 @@ defmodule MinecraftEx.Client.HandshakePackets do # 0x00 Status Request - state=status @deserializable true - packet 0x00 when has_state(socket, :status), as: StatusRequest + defpacket 0x00 when has_state(socket, :status), as: StatusRequest # 0x01 Ping Request - state=status @deserializable true - packet 0x01 when has_state(socket, :status), as: PingRequest do + defpacket 0x01 when has_state(socket, :status), as: PingRequest do field :payload, Long, sign: :signed end end diff --git a/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex index d784a09..b4d0a19 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/client_packets/login_packets.ex @@ -13,7 +13,7 @@ defmodule MinecraftEx.Client.LoginPackets do # 0x00 Login Start - state=login @deserializable true - packet 0x00 when has_state(socket, :login), as: LoginStart do + defpacket 0x00 when has_state(socket, :login), as: LoginStart do field :name, MCString field :player_uuid?, Boolean field :player_uuid, UUID, if: packet.player_uuid? diff --git a/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex index e9188ba..e373576 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/server_packets/handshake_packets.ex @@ -11,13 +11,13 @@ defmodule MinecraftEx.Server.HandshakePackets do # 0x00 Handshake @serializable true - packet 0x00, as: StatusResponse do + defpacket 0x00, as: StatusResponse do field :json, MCString end # 0x00 Handshake @serializable true - packet 0x01, as: PongResponse do + defpacket 0x01, as: PongResponse do field :payload, Long end end diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 6511504..7986914 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -11,10 +11,10 @@ defmodule ElvenGard.Network.PacketSerializer do ## Packets Macros - The `packet` macros (`packet/1`, `packet/2` and `packet/3`) allow users to define - packet structures. + The `packet` macros (`defpacket/1`, `defpacket/2` and `defpacket/3`) allow users to define + packet structures. They always require a packet ID. The alias (which is the name of the generated - packet structure), the guard (with the `when` keyword), and do-block (for + packet structure), the guard (with the `when` keyword), and do-block (for defining fields) are optional. Users can specify guards in packets macros to conditionally match packets based @@ -55,7 +55,7 @@ defmodule ElvenGard.Network.PacketSerializer do field :id, Integer field :name, String end - + # Complex string packet: # - This generate a AliasedModule structure # - The `decode/3` function will match packet with `complex_packet` as packet id ONLY if the state is `:init` @@ -65,7 +65,7 @@ defmodule ElvenGard.Network.PacketSerializer do field :id, Integer field :name, String, fill: true end - + # Simple binary packet # - This generate a KeepAlivePacket structure # - The `decode/3` function will match packet with `0x0000` as packet id @@ -81,43 +81,43 @@ defmodule ElvenGard.Network.PacketSerializer do ## Public API - # packet 0x0000 - defmacro packet(id) when is_packet_id(id) do + # defpacket 0x0000 + defmacro defpacket(id) when is_packet_id(id) do do_packet(id, id_to_name(id), nil, nil, __CALLER__) end - # packet 0x0000 when ... - defmacro packet({:when, _, [id, guards]}) when is_packet_id(id) do + # defpacket 0x0000 when ... + defmacro defpacket({:when, _, [id, guards]}) when is_packet_id(id) do do_packet(id, id_to_name(id), guards, nil, __CALLER__) end - # packet 0x0000, as: ModuleName - defmacro packet(id, as: name) when is_packet_id(id) do + # defpacket 0x0000, as: ModuleName + defmacro defpacket(id, as: name) when is_packet_id(id) do do_packet(id, name, nil, nil, __CALLER__) end - # packet 0x0000 when ..., as: ModuleName - defmacro packet({:when, _, [id, guards]}, as: name) when is_packet_id(id) do + # defpacket 0x0000 when ..., as: ModuleName + defmacro defpacket({:when, _, [id, guards]}, as: name) when is_packet_id(id) do do_packet(id, name, guards, nil, __CALLER__) end - # packet 0x0000 do ... end - defmacro packet(id, do: exp) when is_packet_id(id) do + # defpacket 0x0000 do ... end + defmacro defpacket(id, do: exp) when is_packet_id(id) do do_packet(id, id_to_name(id), nil, exp, __CALLER__) end - # packet 0x0000 when ... do ... end - defmacro packet({:when, _, [id, guards]}, do: exp) when is_packet_id(id) do + # defpacket 0x0000 when ... do ... end + defmacro defpacket({:when, _, [id, guards]}, do: exp) when is_packet_id(id) do do_packet(id, id_to_name(id), guards, exp, __CALLER__) end - # packet 0x0000, as: ModuleName do ... end - defmacro packet(id, [as: name], do: exp) when is_packet_id(id) do + # defpacket 0x0000, as: ModuleName do ... end + defmacro defpacket(id, [as: name], do: exp) when is_packet_id(id) do do_packet(id, name, nil, exp, __CALLER__) end - # packet 0x0000 when ..., as: ModuleName do ... end - defmacro packet({:when, _, [id, guards]}, [as: name], do: exp) when is_packet_id(id) do + # defpacket 0x0000 when ..., as: ModuleName do ... end + defmacro defpacket({:when, _, [id, guards]}, [as: name], do: exp) when is_packet_id(id) do do_packet(id, name, guards, exp, __CALLER__) end @@ -145,9 +145,9 @@ defmodule ElvenGard.Network.PacketSerializer do quote location: :keep do import unquote(__MODULE__), only: [ - packet: 1, - packet: 2, - packet: 3, + defpacket: 1, + defpacket: 2, + defpacket: 3, field: 2, field: 3 ] diff --git a/test/elven_gard/network/packet_schema_test.exs b/test/elven_gard/network/packet_schema_test.exs index 2682963..58ab82b 100644 --- a/test/elven_gard/network/packet_schema_test.exs +++ b/test/elven_gard/network/packet_schema_test.exs @@ -10,7 +10,8 @@ defmodule ElvenGard.Network.PacketSerializerTest do ## Packets def - packet "my_simple_packet" do + @deserializable true + defpacket "my_simple_packet" do field :id, Int field :name, Str field :enabled, Boolean @@ -24,6 +25,10 @@ defmodule ElvenGard.Network.PacketSerializerTest do %{ id: "my_simple_packet", name: MySimplePacket, + parent: ElvenGard.Network.PacketSerializerTest.SimplePackets, + mod: ElvenGard.Network.PacketSerializerTest.SimplePackets.MySimplePacket, + serializable: false, + deserializable: true, guards: nil, fields: [ %{name: :id, type: ElvenGard.Network.CustomTypes.Int, opts: []}, @@ -87,37 +92,48 @@ defmodule ElvenGard.Network.PacketSerializerTest do ## Packets def - packet "no_field" + @deserializable true + defpacket "no_field" - packet "no_field_but_guard" when socket.assigns.state == :foo + @deserializable true + defpacket "no_field_but_guard" when socket.assigns.state == :foo - packet "no_field_but_name", as: NoFieldButName2 + @deserializable true + defpacket "no_field_but_name", as: NoFieldButName2 - packet "no_field_but_name_and_guard" when socket.assigns.state == :foo, + @deserializable true + defpacket "no_field_but_name_and_guard" when socket.assigns.state == :foo, as: NoFieldButNameAndGuard2 - packet "no_field_but_name_and_guard" when socket.assigns.state == :bar, + @deserializable true + defpacket "no_field_but_name_and_guard" when socket.assigns.state == :bar, as: NoFieldButNameAndGuard3 - packet "with_empty_fields", do: :ok + @deserializable true + defpacket "with_empty_fields", do: :ok - packet "with_guards" when socket.assigns.state == :foo do + @deserializable true + defpacket "with_guards" when socket.assigns.state == :foo do field :value, Str end - packet "with_name", as: WithName2 do + @deserializable true + defpacket "with_name", as: WithName2 do field :value, Str end - packet "with_guards_and_name" when socket.assigns.state == :foo, as: WithGuardsAndName2 do + @deserializable true + defpacket "with_guards_and_name" when socket.assigns.state == :foo, as: WithGuardsAndName2 do field :value, Str end - packet "with_options" do + @deserializable true + defpacket "with_options" do field :value, Str, fill: true end - packet "with_condition" do + @deserializable true + defpacket "with_condition" do field :enabled, Boolean field :value, Str, if: packet.enabled end @@ -209,7 +225,9 @@ defmodule ElvenGard.Network.PacketSerializerTest do Code.compile_string(""" defmodule CantCompile do use ElvenGard.Network.PacketSerializer - packet 0x00, do: :ok + + @deserializable true + defpacket 0x00, do: :ok end """) end From 2b250fc28106bcb27b02b7ef17cd49cbc14c361a Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:08:51 +0200 Subject: [PATCH 17/59] :art: Mix format --- examples/minecraft_ex/lib/minecraft_ex/client_packets.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex b/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex index 1eb8973..e6dc5a3 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/client_packets.ex @@ -2,9 +2,9 @@ defmodule MinecraftEx.ClientPackets do @moduledoc """ TODO: MinecraftEx.ClientPackets """ - + import ElvenGard.Network.PacketSerializer, only: [import_packets: 1] - + import_packets MinecraftEx.Client.HandshakePackets import_packets MinecraftEx.Client.LoginPackets end From 4ec78ffba154f324c7ff5e22649c08209711d03a Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:40:06 +0200 Subject: [PATCH 18/59] :recycle: Typo "serialized" -> "encoded" for structures --- examples/echo_server/config/config.exs | 2 +- .../minecraft_ex/endpoint/network_codec.ex | 6 +- .../lib/minecraft_ex/types/enum.ex | 4 +- lib/elven_gard/network/packet_serializer.ex | 70 +++++++++---------- lib/elven_gard/network/socket.ex | 2 +- lib/elven_gard/network/type.ex | 4 +- .../elven_gard/network/packet_schema_test.exs | 41 +++++------ 7 files changed, 64 insertions(+), 65 deletions(-) diff --git a/examples/echo_server/config/config.exs b/examples/echo_server/config/config.exs index d201068..e3b4a99 100644 --- a/examples/echo_server/config/config.exs +++ b/examples/echo_server/config/config.exs @@ -16,6 +16,6 @@ config :echo_server, EchoServer.Endpoint.Protocol, # Here, the packet handler is not needed because we bypass the packet # handling by returning `ignore` in `handle_message/2` packet_handler: :unset, - # Here we are not using packet coder/decoder. + # Here we are not using the network encoder/decoder. # We send raw packets network_codec: :unset diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex index a2d57e1..9a7873f 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex @@ -27,10 +27,10 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do @impl true def deserialize(raw, socket) do {packet_id, rest} = VarInt.decode(raw) - packet = ClientPackets.decode(packet_id, rest, socket) + packet = ClientPackets.deserialize(packet_id, rest, socket) if is_nil(packet) do - raise "unable to decode packet with id #{inspect(packet_id)} - #{inspect(raw)}" + raise "unable to deserialize packet with id #{inspect(packet_id)} - #{inspect(raw)}" end packet @@ -38,7 +38,7 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do @impl true def serialize(struct, socket) when is_struct(struct) do - {packet_id, params} = struct.__struct__.encode(struct) + {packet_id, params} = struct.__struct__.serialize(struct) serialize([VarInt.encode(packet_id), params], socket) end diff --git a/examples/minecraft_ex/lib/minecraft_ex/types/enum.ex b/examples/minecraft_ex/lib/minecraft_ex/types/enum.ex index bbcfc4f..cbd0b70 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/types/enum.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/types/enum.ex @@ -6,7 +6,7 @@ defmodule MinecraftEx.Types.Enum do The list of possible values and how each is encoded as an X must be known from the context. An invalid value sent by either side will usually result - in the client being disconnected with an error or even crashing. + in the client being disconnected with an error or even crashing. """ use ElvenGard.Network.Type @@ -22,7 +22,7 @@ defmodule MinecraftEx.Types.Enum do {from, opts} = Keyword.pop!(opts, :from) {enumerators, opts} = Keyword.pop!(opts, :values) - {value, rest} = apply(from, :decode, [data, opts]) + {value, rest} = from.decode(data, opts) {key, _v} = Enum.find(enumerators, &(elem(&1, 1) == value)) {key, rest} diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 7986914..7c1f7fe 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -4,14 +4,14 @@ defmodule ElvenGard.Network.PacketSerializer do This module provides a DSL (Domain-Specific Language) for defining packet serializers for received packets in a network protocol. It enables users to create structured - packets with specified fields and decode binary data into packet structs. + packets with specified fields and deserialize binary data into packet structs. To learn more about the available macros and how to define packet serializers, refer to the [Packet Serializer DSL documentation](). ## Packets Macros - The `packet` macros (`defpacket/1`, `defpacket/2` and `defpacket/3`) allow users to define + The `defpacket` macros (`defpacket/1`, `defpacket/2` and `defpacket/3`) allow users to define packet structures. They always require a packet ID. The alias (which is the name of the generated packet structure), the guard (with the `when` keyword), and do-block (for @@ -25,13 +25,13 @@ defmodule ElvenGard.Network.PacketSerializer do ## Packet Structure and Decoding - The `packets` macros generate both a packet structure and a `decode/3` function - for each defined packet. The packet structure represents the decoded packet data, - and the `decode/3` function takes binary data and converts it into the structure + The `packets` macros generate both a packet structure and a `deserialize/3` function + for each defined packet. The packet structure represents the deserialized packet data, + and the `deserialize/3` function takes binary data and converts it into the structure based on the defined schema. - The `decode/3` function generated for each packet should be used by - `c:ElvenGard.Network.NetworkCodec.deserialize/2`. + The `deserialize/3` function generated for each packet should be used by + `c:ElvenGard.Network.NetworkCodec.decode/2`. ## Field Macros @@ -49,29 +49,29 @@ defmodule ElvenGard.Network.PacketSerializer do # Simple string packet: # - This generate a SimplePacket structure - # - The `decode/3` function will match all packet with `simple_packet` as packet id + # - The `deserialize/3` function will match all packet with `simple_packet` as packet id # - The structure will have 2 fields: `id` and `name` (a string and an integer) - packet "simple_packet" do + defpacket "simple_packet" do field :id, Integer field :name, String end # Complex string packet: # - This generate a AliasedModule structure - # - The `decode/3` function will match packet with `complex_packet` as packet id ONLY if the state is `:init` + # - The `deserialize/3` function will match packet with `complex_packet` as packet id ONLY if the state is `:init` # - The structure will have 2 fields: `id` and `name` (a string and an integer) # - We're also passing `[fill: true]` as option to our `MyApp.Types.String.decode/2` - packet "complex_packet" when socket.assigns.state == :init, as: AliasedModule do + defpacket "complex_packet" when socket.assigns.state == :init, as: AliasedModule do field :id, Integer field :name, String, fill: true end # Simple binary packet # - This generate a KeepAlivePacket structure - # - The `decode/3` function will match packet with `0x0000` as packet id + # - The `deserialize/3` function will match packet with `0x0000` as packet id # - The structure will have no field # - Here the `:as` option is required because the packet ID is an integer - packet 0x0000, as: KeepAlivePacket + defpacket 0x0000, as: KeepAlivePacket """ @@ -127,17 +127,17 @@ defmodule ElvenGard.Network.PacketSerializer do end defmacro import_packets(mod) do - decode_body_fun = fn %{mod: mod} -> + deserialize_body_fun = fn %{mod: mod} -> quote location: :keep do - unquote(mod).decode(var!(packet_id), var!(data), var!(socket)) + unquote(mod).deserialize(var!(packet_id), var!(data), var!(socket)) end end mod = expand_aliases(mod, __CALLER__) - decode_ast = Enum.map(mod.__schemas__(), &def_decode(&1, decode_body_fun)) + deserialize_ast = Enum.map(mod.__schemas__(), &def_deserialize(&1, deserialize_body_fun)) quote location: :keep do - (unquote_splicing(decode_ast)) + (unquote_splicing(deserialize_ast)) end end @@ -164,18 +164,18 @@ defmodule ElvenGard.Network.PacketSerializer do end defmacro __before_compile__(env) do - decode_body_fun = fn %{id: id, mod: mod} -> + deserialize_body_fun = fn %{id: id, mod: mod} -> quote do - unquote(mod).decode(unquote(id), var!(data), var!(socket)) + unquote(mod).deserialize(unquote(id), var!(data), var!(socket)) end end egn_packets = Module.get_attribute(env.module, :egn_packets) - decode_functions = Enum.map(egn_packets, &def_decode(&1, decode_body_fun)) + deserialize_functions = Enum.map(egn_packets, &def_deserialize(&1, deserialize_body_fun)) quote do unquote(schemas_introspection()) - unquote_splicing(decode_functions) + unquote_splicing(deserialize_functions) end end @@ -249,29 +249,27 @@ defmodule ElvenGard.Network.PacketSerializer do end end - defp def_encode(%{id: id, fields: fields}) do + defp def_serialize(%{id: id, fields: fields}) do fields_ast = Enum.map(fields, fn %{name: name, type: type, opts: opts} -> quote location: :keep do - unquote(type).encode( - Map.fetch!(var!(packet), unquote(name)), - unquote(opts) - ) + unquote(type).encode(Map.fetch!(var!(packet), unquote(name)), unquote(opts)) end end) quote location: :keep, generated: true do - def encode(%__MODULE__{} = var!(packet)) do + def serialize(%__MODULE__{} = var!(packet)) do {unquote(id), unquote(fields_ast)} end end end - defp def_decode(%{id: id, guards: guards} = packet, body_cb) do + defp def_deserialize(%{id: id, guards: guards} = packet, body_cb) do guards = if is_nil(guards), do: true, else: guards quote location: :keep, generated: true do - def decode(var!(packet_id) = unquote(id), var!(data), var!(socket)) when unquote(guards) do + def deserialize(var!(packet_id) = unquote(id), var!(data), var!(socket)) + when unquote(guards) do unquote(body_cb.(packet)) end end @@ -291,7 +289,7 @@ defmodule ElvenGard.Network.PacketSerializer do fields_ast = Enum.map(fields, &decode_field_ast(&1, &1[:opts][:if])) - decode_body_fun = fn %{name: name} -> + deserialize_body_fun = fn %{name: name} -> quote location: :keep do var!(packet) = %__MODULE__{} @@ -308,7 +306,7 @@ defmodule ElvenGard.Network.PacketSerializer do quote location: :keep do defmodule unquote(mod) do - import unquote(parent), except: [decode: 3, encode: 3] + import unquote(parent), except: [deserialize: 3, serialize: 1] # Structure @@ -322,18 +320,18 @@ defmodule ElvenGard.Network.PacketSerializer do def __schema__(:guards), do: unquote(Macro.escape(guards)) def __schema__(:fields), do: unquote(Macro.escape(fields)) - # Encoder/Decoder + # Serializer/Deserializer if unquote(serializable) do - unquote(def_encode(packet)) + unquote(def_serialize(packet)) else - def encode(_), do: raise("unimplemented") + def serialize(_), do: raise("unimplemented") end if unquote(deserializable) do - unquote(def_decode(packet, decode_body_fun)) + unquote(def_deserialize(packet, deserialize_body_fun)) else - def decode(_, _, _), do: raise("unimplemented") + def deserialize(_, _, _), do: raise("unimplemented") end end end diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index 5ccf928..3d76528 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -5,7 +5,7 @@ defmodule ElvenGard.Network.Socket do This module provides functionality for managing a socket in the network protocol. A socket is a connection between the server and a client. It maintains various socket fields, such as the socket ID, socket assigns, transport information, - and the packet encoder used for sending data. + and the packet network encoder used for sending data. ## Socket fields diff --git a/lib/elven_gard/network/type.ex b/lib/elven_gard/network/type.ex index dbddfcf..726b5bf 100644 --- a/lib/elven_gard/network/type.ex +++ b/lib/elven_gard/network/type.ex @@ -68,9 +68,9 @@ defmodule ElvenGard.Network.Type do - `value`: The value of the custom type to be encoded. - `opts`: A keyword list of options that can be used during encoding (optional). - The function should return the encoded binary data as an iodata. + The function should return the encoded binary data as an iodata or a bitstring. """ - @callback encode(value :: any, opts :: keyword) :: iodata + @callback encode(value :: any, opts :: keyword) :: iodata | bitstring defmacro __using__(_env) do quote location: :keep do diff --git a/test/elven_gard/network/packet_schema_test.exs b/test/elven_gard/network/packet_schema_test.exs index 58ab82b..1f032b3 100644 --- a/test/elven_gard/network/packet_schema_test.exs +++ b/test/elven_gard/network/packet_schema_test.exs @@ -60,13 +60,14 @@ defmodule ElvenGard.Network.PacketSerializerTest do ] end - describe "decode/3" do + describe "deserialize/3" do test "is defined" do - assert function_exported?(SimplePackets, :decode, 3) + assert function_exported?(SimplePackets, :deserialize, 3) end test "parse a binary and returns a structure" do - packet = SimplePackets.decode("my_simple_packet", "1337 Admin 1 2023-07-21", %Socket{}) + packet = + SimplePackets.deserialize("my_simple_packet", "1337 Admin 1 2023-07-21", %Socket{}) assert packet.__struct__ == __MODULE__.MySimplePacket assert packet.id == 1337 @@ -77,7 +78,7 @@ defmodule ElvenGard.Network.PacketSerializerTest do test "raise an error if remaining bytes" do assert_raise RuntimeError, ~r/remaining bytes for /, fn -> - SimplePackets.decode("my_simple_packet", "1337 Admin 1 2023-07-21 foo", %Socket{}) + SimplePackets.deserialize("my_simple_packet", "1337 Admin 1 2023-07-21 foo", %Socket{}) end end end @@ -140,67 +141,67 @@ defmodule ElvenGard.Network.PacketSerializerTest do ## Tests - describe "decode packet with" do + describe "deserialize packet with" do test "no field" do - assert packet = decode("no_field") + assert packet = deserialize("no_field") assert packet.__struct__ == __MODULE__.NoField end test "no field but guards" do - assert packet = decode("no_field_but_guard", state: :foo) + assert packet = deserialize("no_field_but_guard", state: :foo) assert packet.__struct__ == __MODULE__.NoFieldButGuard - assert_raise FunctionClauseError, fn -> decode("no_field_but_guard", state: :bar) end + assert_raise FunctionClauseError, fn -> deserialize("no_field_but_guard", state: :bar) end end test "no field but name" do - assert packet = decode("no_field_but_name") + assert packet = deserialize("no_field_but_name") assert packet.__struct__ == __MODULE__.NoFieldButName2 end test "no field but name and guard" do - assert packet = decode("no_field_but_name_and_guard", state: :foo) + assert packet = deserialize("no_field_but_name_and_guard", state: :foo) assert packet.__struct__ == __MODULE__.NoFieldButNameAndGuard2 - assert packet = decode("no_field_but_name_and_guard", state: :bar) + assert packet = deserialize("no_field_but_name_and_guard", state: :bar) assert packet.__struct__ == __MODULE__.NoFieldButNameAndGuard3 end test "empty field" do - assert packet = decode("with_empty_fields") + assert packet = deserialize("with_empty_fields") assert packet.__struct__ == __MODULE__.WithEmptyFields end test "guards" do - assert packet = decode("with_guards", raw: "bar", state: :foo) + assert packet = deserialize("with_guards", raw: "bar", state: :foo) assert packet.__struct__ == __MODULE__.WithGuards assert packet.value == "bar" end test "name" do - assert packet = decode("with_name", raw: "bar") + assert packet = deserialize("with_name", raw: "bar") assert packet.__struct__ == __MODULE__.WithName2 assert packet.value == "bar" end test "guards and name" do - assert packet = decode("with_guards_and_name", raw: "bar", state: :foo) + assert packet = deserialize("with_guards_and_name", raw: "bar", state: :foo) assert packet.__struct__ == __MODULE__.WithGuardsAndName2 assert packet.value == "bar" end test "options" do - assert packet = decode("with_options", raw: "foo bar") + assert packet = deserialize("with_options", raw: "foo bar") assert packet.__struct__ == __MODULE__.WithOptions assert packet.value == "foo bar" end test "condition" do - assert packet = decode("with_condition", raw: "0") + assert packet = deserialize("with_condition", raw: "0") assert packet.__struct__ == __MODULE__.WithCondition assert packet.enabled == false assert packet.value == nil - assert packet = decode("with_condition", raw: "1 foobar") + assert packet = deserialize("with_condition", raw: "1 foobar") assert packet.__struct__ == __MODULE__.WithCondition assert packet.enabled == true assert packet.value == "foobar" @@ -209,9 +210,9 @@ defmodule ElvenGard.Network.PacketSerializerTest do ## Helpers - defp decode(name, opts \\ []) do + defp deserialize(name, opts \\ []) do assigns = if opts[:state], do: %{state: opts[:state]}, else: nil - StringPackets.decode(name, opts[:raw] || "", %Socket{assigns: assigns}) + StringPackets.deserialize(name, opts[:raw] || "", %Socket{assigns: assigns}) end end From ba421a99b87a3b81501707bc50b9f29241df390c Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:49:38 +0200 Subject: [PATCH 19/59] :ambulance: Fix do..end expansion --- lib/elven_gard/network/packet_serializer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 7c1f7fe..632cb9b 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -211,7 +211,7 @@ defmodule ElvenGard.Network.PacketSerializer do defp do_packet(id, name, guards, exp, caller) do guard_env = %{caller | context: :guard} guards = Macro.postwalk(guards, &Macro.expand(&1, guard_env)) - exp = expand_aliases(exp, caller) + exp = Macro.postwalk(exp, &Macro.expand(&1, caller)) quote location: :keep do if not @serializable and not @deserializable do From f833a7ea0d460a0f85deececb8bba1d1fb9c2424 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:57:35 +0200 Subject: [PATCH 20/59] :sparkles: Support default option for fields --- lib/elven_gard/network/packet_serializer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 632cb9b..7584e57 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -310,7 +310,7 @@ defmodule ElvenGard.Network.PacketSerializer do # Structure - defstruct Enum.map(unquote(Macro.escape(fields)), & &1.name) + defstruct Enum.map(unquote(Macro.escape(fields)), &{&1.name, &1[:opts][:default]}) # Introspection From 149efd81829b4ebe95bebf484c3806d09012357d Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Fri, 11 Aug 2023 22:23:04 +0200 Subject: [PATCH 21/59] :ambulance: Fix render return type --- lib/elven_gard/network/view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elven_gard/network/view.ex b/lib/elven_gard/network/view.ex index f72ddc6..6847a66 100644 --- a/lib/elven_gard/network/view.ex +++ b/lib/elven_gard/network/view.ex @@ -44,7 +44,7 @@ defmodule ElvenGard.Network.View do The function should return an iodata to be sent to the client. """ - @callback render(name, params) :: iodata() + @callback render(name, params) :: iodata() | struct() when name: atom() | String.t(), params: map() | Keyword.t() @doc false From 85f46c9e1eab8361861789439e43557d4a1259d1 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sat, 12 Aug 2023 01:01:19 +0200 Subject: [PATCH 22/59] :sparkles: enforce_keys for packets --- lib/elven_gard/network/packet_serializer.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 7584e57..4dcfa8d 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -291,7 +291,7 @@ defmodule ElvenGard.Network.PacketSerializer do deserialize_body_fun = fn %{name: name} -> quote location: :keep do - var!(packet) = %__MODULE__{} + var!(packet) = %{} unquote_splicing(fields_ast) @@ -300,7 +300,7 @@ defmodule ElvenGard.Network.PacketSerializer do raise "remaining bytes for #{inspect(__MODULE__)}.#{iname}: #{inspect(var!(data))}" end - var!(packet) + struct!(__MODULE__, var!(packet)) end end @@ -310,6 +310,9 @@ defmodule ElvenGard.Network.PacketSerializer do # Structure + @enforce_keys for f <- unquote(Macro.escape(fields)), + is_nil(f[:opts][:default]), + do: f.name defstruct Enum.map(unquote(Macro.escape(fields)), &{&1.name, &1[:opts][:default]}) # Introspection From 4981aada12d28a03c3cefb21b7616799ab39fcf8 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sat, 12 Aug 2023 12:02:16 +0200 Subject: [PATCH 23/59] :recycle: Remove unused callback --- lib/elven_gard/network/endpoint/protocol.ex | 15 ++------------- .../elven_gard/network/endpoint/protocol_test.exs | 6 ------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index d4a3c64..23cbcd8 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -26,16 +26,9 @@ defmodule ElvenGard.Network.Endpoint.Protocol do | {:ok, stop_reason :: term, new_socket} when new_socket: term - @doc "Called when an error occurs and specifies what to do" - @callback handle_error(reason :: term, socket :: Socket.t()) :: - {:ignore, new_socket} - | {:stop, reason :: term, new_socket} - when new_socket: term - @optional_callbacks handle_init: 1, handle_message: 2, - handle_halt: 2, - handle_error: 2 + handle_halt: 2 ## Public API @@ -170,13 +163,9 @@ defmodule ElvenGard.Network.Endpoint.Protocol do @impl true def handle_halt(_reason, socket), do: {:ok, socket} - @impl true - def handle_error(reason, socket), do: {:stop, reason, socket} - defoverridable handle_init: 1, handle_message: 2, - handle_halt: 2, - handle_error: 2 + handle_halt: 2 end end end diff --git a/test/elven_gard/network/endpoint/protocol_test.exs b/test/elven_gard/network/endpoint/protocol_test.exs index 6a29e43..5db3196 100644 --- a/test/elven_gard/network/endpoint/protocol_test.exs +++ b/test/elven_gard/network/endpoint/protocol_test.exs @@ -46,12 +46,6 @@ defmodule ElvenGard.Network.ProtocolTest do send(socket.assigns[:link], {:handle_halt, reason}) {:ok, socket} end - - @impl true - def handle_error(reason, socket) do - send(socket.assigns[:link], {:handle_error, reason}) - {:stop, reason, socket} - end end setup_all do From dfe9f4585cb2147246baf244a153f60b6799bc26 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sat, 12 Aug 2023 13:39:02 +0200 Subject: [PATCH 24/59] :sparkles: next callback now have the socket as param --- .../minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex | 4 ++-- lib/elven_gard/network/endpoint/protocol.ex | 2 +- lib/elven_gard/network/network_codec.ex | 3 ++- test/support/dummy_encoder.ex | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex index 9a7873f..c436e06 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex @@ -9,9 +9,9 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do alias MinecraftEx.ClientPackets @impl true - def next(<<>>), do: {nil, <<>>} + def next(<<>>, _socket), do: {nil, <<>>} - def next(message) do + def next(message, _socket) do {length, rest} = VarInt.decode(message) case byte_size(rest) >= length do diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index 23cbcd8..20ce2b1 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -112,7 +112,7 @@ defmodule ElvenGard.Network.Endpoint.Protocol do defp handlers(), do: env_config()[:packet_handler] defp packet_loop(data, socket) do - with {:next, {raw, rest}} when not is_nil(raw) <- {:next, codec().next(data)}, + with {:next, {raw, rest}} when not is_nil(raw) <- {:next, codec().next(data, socket)}, struct <- codec().deserialize(raw, socket), {:handle, {:cont, new_socket}} <- {:handle, handlers().handle_packet(struct, socket)} do packet_loop(rest, new_socket) diff --git a/lib/elven_gard/network/network_codec.ex b/lib/elven_gard/network/network_codec.ex index 58afa67..bcd0a16 100644 --- a/lib/elven_gard/network/network_codec.ex +++ b/lib/elven_gard/network/network_codec.ex @@ -10,7 +10,8 @@ defmodule ElvenGard.Network.NetworkCodec do The result will be sent to the `c:deserialize/2` callback """ - @callback next(raw :: bitstring) :: {packet_raw :: bitstring, remaining :: bitstring} + @callback next(raw :: bitstring, socket :: Socket.t()) :: + {packet_raw :: bitstring, remaining :: bitstring} @doc "Deserializes a packet" @callback deserialize(raw :: bitstring, socket :: Socket.t()) :: map | struct diff --git a/test/support/dummy_encoder.ex b/test/support/dummy_encoder.ex index 827ab4d..4b9559c 100644 --- a/test/support/dummy_encoder.ex +++ b/test/support/dummy_encoder.ex @@ -4,7 +4,7 @@ defmodule ElvenGard.Network.DummyEncoder do @behaviour ElvenGard.Network.NetworkCodec @impl true - def next(_raw), do: raise("unimplemented") + def next(_raw, _socket), do: raise("unimplemented") @impl true def deserialize(_raw, _socket), do: raise("unimplemented") From e6cb36d4944ad78ae4069faf9cb5201d7f467ecb Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:22:28 +0200 Subject: [PATCH 25/59] :sparkles: fail fast for packet_loop --- lib/elven_gard/network/endpoint/protocol.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index 20ce2b1..ac2feca 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -111,6 +111,8 @@ defmodule ElvenGard.Network.Endpoint.Protocol do defp codec(), do: env_config()[:network_codec] defp handlers(), do: env_config()[:packet_handler] + defp packet_loop(<<>>, socket), do: {:noreply, socket} + defp packet_loop(data, socket) do with {:next, {raw, rest}} when not is_nil(raw) <- {:next, codec().next(data, socket)}, struct <- codec().deserialize(raw, socket), From a4bff33b1678e95f7a59c420ffd745c15142f265 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:46:52 +0200 Subject: [PATCH 26/59] :construction: WIP Prepare for release --- TODOLIST.md | 7 ------- lib/elven_gard/network/endpoint.ex | 21 ++++----------------- lib/elven_gard/network/packet_serializer.ex | 1 + test/elven_gard/network/endpoint_test.exs | 9 --------- 4 files changed, 5 insertions(+), 33 deletions(-) delete mode 100644 TODOLIST.md diff --git a/TODOLIST.md b/TODOLIST.md deleted file mode 100644 index e6248b2..0000000 --- a/TODOLIST.md +++ /dev/null @@ -1,7 +0,0 @@ -# TODOLIST - -- [ ] Fix Endpoint start_link and child_spec -- [ ] Serializers for `ElvenGard.Network.Socket` -- [ ] Rewrite the doc for `ElvenGard.Network.Socket` -- [ ] Use `c:handle_error/2` in `ElvenGard.Network.Endpoint.Protocol` -- [ ] Check if the lib is working in clustered mode diff --git a/lib/elven_gard/network/endpoint.ex b/lib/elven_gard/network/endpoint.ex index 76b8b0f..106882b 100644 --- a/lib/elven_gard/network/endpoint.ex +++ b/lib/elven_gard/network/endpoint.ex @@ -36,10 +36,12 @@ defmodule ElvenGard.Network.Endpoint do end @doc """ - Returns the child specification to start the endpoint - under a supervision tree. + Returns a specification to start this module under a supervisor. + + See `Supervisor`. """ def child_spec(opts) do + # Not sure if the is a better way to do this (call a callback on Endpoint start) if opts[:ignore_init] != true do :ok = handle_start(@config) end @@ -53,21 +55,6 @@ defmodule ElvenGard.Network.Endpoint do ) end - @doc """ - Starts the endpoint. - """ - def start_link(_opts) do - :ok = handle_start(@config) - - :ranch.start_listener( - __listener_name__(), - @config[:transport], - @config[:transport_opts], - @config[:protocol], - @config[:protocol_opts] - ) - end - @doc """ Returns the Endpoint's configuration. """ diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 4dcfa8d..8586117 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -275,6 +275,7 @@ defmodule ElvenGard.Network.PacketSerializer do end end + # credo:disable-for-next-line defp def_structure(packet) do %{ id: id, diff --git a/test/elven_gard/network/endpoint_test.exs b/test/elven_gard/network/endpoint_test.exs index 42b1732..789e40a 100644 --- a/test/elven_gard/network/endpoint_test.exs +++ b/test/elven_gard/network/endpoint_test.exs @@ -119,13 +119,4 @@ defmodule ElvenGard.Network.EndpointTest do refute_receive :handle_start end end - - describe "start_link/0" do - test "call c:handle_start/1" do - {:ok, _} = MyEndpoint.start_link([]) - assert_received :handle_start - # Crash tests sometimes ??? - # GenServer.stop(endpoint) - end - end end From ce7028cac69f7ec7221eae25c1134531d5400591 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:53:21 +0200 Subject: [PATCH 27/59] :construction: WIP Prepare for release (2) --- TODOLIST.md | 5 +++++ .../lib/minecraft_ex/endpoint/packet_handlers.ex | 2 +- .../lib/minecraft_ex/{endpoint => }/packet_views.ex | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 TODOLIST.md rename examples/minecraft_ex/lib/minecraft_ex/{endpoint => }/packet_views.ex (78%) diff --git a/TODOLIST.md b/TODOLIST.md new file mode 100644 index 0000000..5f13f20 --- /dev/null +++ b/TODOLIST.md @@ -0,0 +1,5 @@ +# Todo list + +- Abstract the message handler and provide a generic way to handle all transport (ranch, gen_tcp, gen_udp, ...) +- Refacto ElvenGard.Network.PacketSerializer +- Refacto ElvenGard.Network.Endpoint.Protocol diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex index b586d18..19396a2 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex @@ -14,7 +14,7 @@ defmodule MinecraftEx.Endpoint.PacketHandlers do StatusRequest } - alias MinecraftEx.Endpoint.PacketViews + alias MinecraftEx.PacketViews def handle_packet(%Handshake{} = packet, socket) do {:cont, assign(socket, :state, packet.next_state)} diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex b/examples/minecraft_ex/lib/minecraft_ex/packet_views.ex similarity index 78% rename from examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex rename to examples/minecraft_ex/lib/minecraft_ex/packet_views.ex index b232a0f..f89bbcc 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_views.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/packet_views.ex @@ -1,6 +1,6 @@ -defmodule MinecraftEx.Endpoint.PacketViews do +defmodule MinecraftEx.PacketViews do @moduledoc """ - Documentation for MinecraftEx.Endpoint.PacketViews + Documentation for MinecraftEx.PacketViews """ use ElvenGard.Network.View From c45142724608970e7f9512baeb47eeda036196a3 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 14:27:55 +0200 Subject: [PATCH 28/59] :memo: Update doc for ElvenGard.Network.PacketSerializer --- lib/elven_gard/network/packet_serializer.ex | 74 +++++++-------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index 8586117..ad617e7 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -1,37 +1,35 @@ defmodule ElvenGard.Network.PacketSerializer do @moduledoc ~S""" - Packet Serializer DSL for defining received packet structures. + Packet Serializer DSL for defining packet structures. This module provides a DSL (Domain-Specific Language) for defining packet serializers - for received packets in a network protocol. It enables users to create structured - packets with specified fields and deserialize binary data into packet structs. + for both received and sent packets in a network protocol. It enables users to create + structured packets with specified fields and decode binary data into packet structs. To learn more about the available macros and how to define packet serializers, refer - to the [Packet Serializer DSL documentation](). + to the [Packet Serializer DSL guide](). - ## Packets Macros + ## Packet Macros - The `defpacket` macros (`defpacket/1`, `defpacket/2` and `defpacket/3`) allow users to define - packet structures. - They always require a packet ID. The alias (which is the name of the generated - packet structure), the guard (with the `when` keyword), and do-block (for - defining fields) are optional. + The `defpacket` macros (`defpacket/1`, `defpacket/2`, and `defpacket/3`) allow users + to define packet structures. They require a packet ID and an alias (which is the name + of the generated packet structure). The guard (with the `when` keyword) and do-block + (for defining fields) are optional. - Users can specify guards in packets macros to conditionally match packets based - on a condition (often using socket assigns). + Users can specify guards in packet macros to conditionally match packets based on a + condition (often using socket assigns). - For more details on using the packets macros, please refer to the guide at - or to examples. + For more details on using the packet macros, please refer to the guide at + . - ## Packet Structure and Decoding + ## Packet Structure, Serialization, and Deserialization - The `packets` macros generate both a packet structure and a `deserialize/3` function - for each defined packet. The packet structure represents the deserialized packet data, - and the `deserialize/3` function takes binary data and converts it into the structure - based on the defined schema. + The `defpacket` macros can generate a packet structure, a `deserialize/3` function for + deserializing binary data into the packet structure, and a `serialize/1` function for + generating the serialized binary representation of the packet. - The `deserialize/3` function generated for each packet should be used by - `c:ElvenGard.Network.NetworkCodec.decode/2`. + The `deserialize/3` function should be used for decoding received packets, and the + `serialize/1` function should be used for generating packets to be sent over the network. ## Field Macros @@ -40,38 +38,12 @@ defmodule ElvenGard.Network.PacketSerializer do - `field/2`: Define a field with a name and type. - `field/3`: Define a field with a name, type, and decoding options. - ## Examples + ## Decorators - defmodule MyApp.Endpoint.PacketSerializers do - use ElvenGard.Network.PacketSerializer + The following decorators can be used to specify serialization and deserialization properties: - alias MyApp.Types.{Integer, String} - - # Simple string packet: - # - This generate a SimplePacket structure - # - The `deserialize/3` function will match all packet with `simple_packet` as packet id - # - The structure will have 2 fields: `id` and `name` (a string and an integer) - defpacket "simple_packet" do - field :id, Integer - field :name, String - end - - # Complex string packet: - # - This generate a AliasedModule structure - # - The `deserialize/3` function will match packet with `complex_packet` as packet id ONLY if the state is `:init` - # - The structure will have 2 fields: `id` and `name` (a string and an integer) - # - We're also passing `[fill: true]` as option to our `MyApp.Types.String.decode/2` - defpacket "complex_packet" when socket.assigns.state == :init, as: AliasedModule do - field :id, Integer - field :name, String, fill: true - end - - # Simple binary packet - # - This generate a KeepAlivePacket structure - # - The `deserialize/3` function will match packet with `0x0000` as packet id - # - The structure will have no field - # - Here the `:as` option is required because the packet ID is an integer - defpacket 0x0000, as: KeepAlivePacket + - `@serializable true`: Marks the packet as serializable for sending over the network. + - `@deserializable true`: Marks the packet as deserializable for receiving from the network. """ From 731c9e3e92fa13f235477eaeecd463e54ff77b20 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 14:31:07 +0200 Subject: [PATCH 29/59] :memo: Update doc for ElvenGard.Network.Socket --- lib/elven_gard/network/socket.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index 3d76528..7c08340 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -65,7 +65,7 @@ defmodule ElvenGard.Network.Socket do ## Examples - iex> ElvenGard.Network.Socket.send(socket, {:login_response, %{status: 200, message: "Welcome!"}}) + iex> ElvenGard.Network.Socket.send(socket, %LoginResponse{status: 200, message: "Welcome!"}) :ok """ From 94397abc808a2abc443d051cb6255426e95ee8df Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 14:56:43 +0200 Subject: [PATCH 30/59] :memo: Update doc for ElvenGard.Network.NetworkCodec --- .../minecraft_ex/endpoint/network_codec.ex | 8 +-- lib/elven_gard/network/endpoint/protocol.ex | 2 +- lib/elven_gard/network/network_codec.ex | 58 ++++++++++++++++--- lib/elven_gard/network/socket.ex | 4 +- test/support/dummy_encoder.ex | 4 +- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex index c436e06..8f71c62 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/network_codec.ex @@ -25,7 +25,7 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do end @impl true - def deserialize(raw, socket) do + def decode(raw, socket) do {packet_id, rest} = VarInt.decode(raw) packet = ClientPackets.deserialize(packet_id, rest, socket) @@ -37,12 +37,12 @@ defmodule MinecraftEx.Endpoint.NetworkCodec do end @impl true - def serialize(struct, socket) when is_struct(struct) do + def encode(struct, socket) when is_struct(struct) do {packet_id, params} = struct.__struct__.serialize(struct) - serialize([VarInt.encode(packet_id), params], socket) + encode([VarInt.encode(packet_id), params], socket) end - def serialize(raw, _socket) when is_list(raw) do + def encode(raw, _socket) when is_list(raw) do bin = :binary.list_to_bin(raw) packet_length = bin |> byte_size() |> VarInt.encode([]) [<> | bin] diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index ac2feca..f9147be 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -115,7 +115,7 @@ defmodule ElvenGard.Network.Endpoint.Protocol do defp packet_loop(data, socket) do with {:next, {raw, rest}} when not is_nil(raw) <- {:next, codec().next(data, socket)}, - struct <- codec().deserialize(raw, socket), + struct <- codec().decode(raw, socket), {:handle, {:cont, new_socket}} <- {:handle, handlers().handle_packet(struct, socket)} do packet_loop(rest, new_socket) else diff --git a/lib/elven_gard/network/network_codec.ex b/lib/elven_gard/network/network_codec.ex index bcd0a16..8383772 100644 --- a/lib/elven_gard/network/network_codec.ex +++ b/lib/elven_gard/network/network_codec.ex @@ -1,21 +1,63 @@ defmodule ElvenGard.Network.NetworkCodec do - @moduledoc ~S""" - Define a behaviour for packet encoding and decoding + @moduledoc """ + Behavior for Defining Packet Encoding and Decoding + + Implementations of this behavior are responsible for parsing incoming + raw binary data into structured packets and encoding structured packets into + binary data for transmission over the network. + + For more information on how to implement this behavior and use packet encoding + and decoding, please refer to the [NetworkCodec guide] + (). """ alias ElvenGard.Network.Socket @doc """ - Returns the first packet found in a raw binary + Identifies and extracts the first packet from raw binary data. + + This function searches for the first packet within the raw binary data and + returns it along with the remaining binary data. The extracted packet will + be passed to the `decode/2` callback for further processing. + + ## Examples + + raw = <> + {packet1, remaining} = ElvenGard.Network.NetworkCodec.next(raw, socket) - The result will be sent to the `c:deserialize/2` callback """ @callback next(raw :: bitstring, socket :: Socket.t()) :: {packet_raw :: bitstring, remaining :: bitstring} - @doc "Deserializes a packet" - @callback deserialize(raw :: bitstring, socket :: Socket.t()) :: map | struct + @doc """ + Decodes a packet from raw binary data. + + This callback function is responsible for decoding a packet from the provided + raw binary data. The decoded packet should be returned as a struct containing + the parsed fields. + + ## Examples + + raw_packet = <<1::8, 123::8, "some data"::binary>> # Example raw packet data + %MessageStruct{id: 123, data: "some data"} = ElvenGard.Network.NetworkCodec.decode(raw_packet, socket) - @doc "Serialize a `ElvenGard.Network.View`" - @callback serialize(raw :: iodata, socket :: Socket.t()) :: iodata + """ + @callback decode(raw :: bitstring, socket :: Socket.t()) :: struct + + @doc """ + Encodes a packet for transmission. + + This callback function is responsible for encoding a structured packet or raw + binary data into the binary format suitable for transmission over the network. + The resulting encoded data, in the form of `iodata()`, can be directly sent + over the network. + + ## Examples + + packet = %MessageStruct{id: 1, data: "Hello"} + <> = ElvenGard.Network.NetworkCodec.encode(packet, socket) + + """ + @callback encode(packet | raw, socket :: Socket.t()) :: iodata() + when packet: struct(), raw: iodata() end diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index 7c08340..b09ba62 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -69,7 +69,7 @@ defmodule ElvenGard.Network.Socket do :ok """ - @spec send(Socket.t(), any) :: :ok | {:error, atom} + @spec send(Socket.t(), struct() | iodata()) :: :ok | {:error, atom} def send(%Socket{encoder: :unset} = socket, data) do %Socket{transport: transport, transport_pid: transport_pid} = socket transport.send(transport_pid, data) @@ -77,7 +77,7 @@ defmodule ElvenGard.Network.Socket do def send(%Socket{} = socket, message) do %Socket{transport: transport, transport_pid: transport_pid, encoder: encoder} = socket - data = encoder.serialize(message, socket) + data = encoder.encode(message, socket) transport.send(transport_pid, data) end diff --git a/test/support/dummy_encoder.ex b/test/support/dummy_encoder.ex index 4b9559c..c017430 100644 --- a/test/support/dummy_encoder.ex +++ b/test/support/dummy_encoder.ex @@ -7,8 +7,8 @@ defmodule ElvenGard.Network.DummyEncoder do def next(_raw, _socket), do: raise("unimplemented") @impl true - def deserialize(_raw, _socket), do: raise("unimplemented") + def decode(_raw, _socket), do: raise("unimplemented") @impl true - def serialize(raw, _socket), do: raw + def encode(raw, _socket), do: raw end From 93cd541cad5faf13a1cdd53c853dd314233c2733 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:13:54 +0200 Subject: [PATCH 31/59] :memo: Update doc for ElvenGard.Network --- README.md | 2 ++ lib/elven_gard/network.ex | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 168ef7d..cbb4c76 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![Build Status](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml/badge.svg?branch=master)](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml) [![Coverage Status](https://coveralls.io/repos/github/ImNotAVirus/elvengard_network/badge.svg?branch=master)](https://coveralls.io/github/ImNotAVirus/elvengard_network?branch=master) + + ## What is ElvenGard Currently, all independent developers wishing to create a Multiplayer game have already asked themselves the question of how to make the server part easily while being "solid" (minimum crash, latency, ...). diff --git a/lib/elven_gard/network.ex b/lib/elven_gard/network.ex index a8ea8f0..4ec0665 100644 --- a/lib/elven_gard/network.ex +++ b/lib/elven_gard/network.ex @@ -1,5 +1,6 @@ defmodule ElvenGard.Network do - @moduledoc """ - Documentation for `ElvenGard.Network`. - """ + @moduledoc "README.md" + |> File.read!() + |> String.split("") + |> Enum.fetch!(1) end From d935c1dd0ffb8acd8b045e57a1e40788e8ff0a2c Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:23:16 +0200 Subject: [PATCH 32/59] :memo: Update doc for ElvenGard.Network.Endpoint --- lib/elven_gard/network/endpoint.ex | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/elven_gard/network/endpoint.ex b/lib/elven_gard/network/endpoint.ex index 106882b..c9036b3 100644 --- a/lib/elven_gard/network/endpoint.ex +++ b/lib/elven_gard/network/endpoint.ex @@ -1,6 +1,13 @@ defmodule ElvenGard.Network.Endpoint do - @moduledoc ~S""" - TODO: Documentation for ElvenGard.Network.Endpoint + @moduledoc """ + Wrapper on top of Ranch's [listeners](https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/). + + This module provides a wrapper around the Ranch library to define network + endpoints. Endpoints are crucial for managing incoming connections and + handling network traffic efficiently. + + For in-depth information on how to use and configure network endpoints, please + refer to the [Endpoint documentation](). """ @doc "Called just before starting the ranch listener" From 215daf6fc115e79995b7147df885e7f0eaa03f72 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:36:08 +0200 Subject: [PATCH 33/59] :memo: Update doc for ElvenGard.Network.Endpoint.Protocol --- lib/elven_gard/network/endpoint/protocol.ex | 35 ++++++++++++++++++--- lib/elven_gard/network/exceptions.ex | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index f9147be..3c01c04 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -1,18 +1,40 @@ defmodule ElvenGard.Network.Endpoint.Protocol do @moduledoc """ - TODO: Documentation + Wrapper on top of Ranch's [protocols](https://ninenines.eu/docs/en/ranch/2.1/guide/protocols/). + + This module defines a protocol behavior to handle incoming connections in the + ElvenGard.Network library. It provides callbacks for initializing, handling + incoming messages, and handling connection termination. + + This protocol behavior serves as a wrapper around Ranch protocols, providing + a structured way to implement connection handling within ElvenGard.Network. + + For detailed information on implementing and using network protocols + with ElvenGard.Network, please refer to the [Endpoint Protocol guide] + (). """ alias ElvenGard.Network.Socket - @doc "Called just before entering the GenServer loop" + @doc """ + Callback called just before entering the GenServer loop. + + This callback is invoked when a new connection is established and before the + GenServer loop starts processing messages. + + For the return values, see `c:GenServer.init/1` + """ @callback handle_init(socket :: Socket.t()) :: {:ok, new_socket} | {:ok, new_socket, timeout | :hibernate | {:continue, continue_arg}} | {:stop, reason :: term, new_socket} when new_socket: Socket.t(), continue_arg: term - @doc "Called just after receiving a message" + @doc """ + Callback called just after receiving a message. + + For the return values, see `c:GenServer.handle_info/2` + """ @callback handle_message(message :: binary, socket :: Socket.t()) :: :ignore | {:ignore, new_socket} @@ -20,7 +42,12 @@ defmodule ElvenGard.Network.Endpoint.Protocol do | {:stop, reason :: term, new_socket} when new_socket: Socket.t() - @doc "Called after the socket connection is closed and before the GenServer shutdown" + @doc """ + Callback called after the socket connection is closed and before the GenServer + shutdown. + + For the return values, see `c:GenServer.handle_info/2` + """ @callback handle_halt(reason :: term, socket :: Socket.t()) :: {:ok, new_socket} | {:ok, stop_reason :: term, new_socket} diff --git a/lib/elven_gard/network/exceptions.ex b/lib/elven_gard/network/exceptions.ex index 15068fb..19ccda6 100644 --- a/lib/elven_gard/network/exceptions.ex +++ b/lib/elven_gard/network/exceptions.ex @@ -1,6 +1,6 @@ defmodule ElvenGard.Network.UnknownViewError do @moduledoc ~S""" - Exception raised when a view is not found + Exception raised when a View is not found """ defexception [:parent, :type] From f37d6dddd6d647c968d93e003ea8e22f621d80a8 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:49:35 +0200 Subject: [PATCH 34/59] :memo: Update doc --- lib/elven_gard/network/endpoint.ex | 2 +- lib/elven_gard/network/endpoint/protocol.ex | 2 +- lib/elven_gard/network/network_codec.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elven_gard/network/endpoint.ex b/lib/elven_gard/network/endpoint.ex index c9036b3..ddf4ed7 100644 --- a/lib/elven_gard/network/endpoint.ex +++ b/lib/elven_gard/network/endpoint.ex @@ -1,5 +1,5 @@ defmodule ElvenGard.Network.Endpoint do - @moduledoc """ + @moduledoc ~S""" Wrapper on top of Ranch's [listeners](https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/). This module provides a wrapper around the Ranch library to define network diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index 3c01c04..98b1308 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -1,5 +1,5 @@ defmodule ElvenGard.Network.Endpoint.Protocol do - @moduledoc """ + @moduledoc ~S""" Wrapper on top of Ranch's [protocols](https://ninenines.eu/docs/en/ranch/2.1/guide/protocols/). This module defines a protocol behavior to handle incoming connections in the diff --git a/lib/elven_gard/network/network_codec.ex b/lib/elven_gard/network/network_codec.ex index 8383772..e0681f4 100644 --- a/lib/elven_gard/network/network_codec.ex +++ b/lib/elven_gard/network/network_codec.ex @@ -1,5 +1,5 @@ defmodule ElvenGard.Network.NetworkCodec do - @moduledoc """ + @moduledoc ~S""" Behavior for Defining Packet Encoding and Decoding Implementations of this behavior are responsible for parsing incoming From aca5ea8661c031eea1d5aca2e127148a03c6019f Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:02:31 +0200 Subject: [PATCH 35/59] :memo: Update doc (2) --- lib/elven_gard/network/uuid.ex | 5 ++--- mix.exs | 30 ++++++++++++++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/elven_gard/network/uuid.ex b/lib/elven_gard/network/uuid.ex index b8d25a7..96bde36 100644 --- a/lib/elven_gard/network/uuid.ex +++ b/lib/elven_gard/network/uuid.ex @@ -1,7 +1,6 @@ defmodule ElvenGard.Network.UUID do - @moduledoc ~S""" - UUID helpers inspired by `zyro/elixir-uuid` - """ + @moduledoc false + # UUID helpers inspired by `zyro/elixir-uuid` ## Public API diff --git a/mix.exs b/mix.exs index c136875..3414870 100644 --- a/mix.exs +++ b/mix.exs @@ -63,23 +63,37 @@ defmodule ElvenGard.Network.MixProject do # logo: "path/to/logo.png", extra_section: "GUIDES", extras: extras(), - groups_for_extras: groups_for_extras() - # groups_for_modules: [ - # "Textual protocol specs": ~r/ElvenGard\.Protocol\.Textual\.?/, - # "Binary protocol specs": ~r/ElvenGard\.Protocol\.Binary\.?/, - # PacketHandler: ~r/ElvenGard\.PacketHandler\./ - # ] + groups_for_extras: groups_for_extras(), + groups_for_modules: groups_for_modules() ] end - defp extras do + defp extras() do ["README.md": [title: "Overview"]] ++ Path.wildcard("guides/**/*.md") end - defp groups_for_extras do + defp groups_for_extras() do [ Introduction: ~r/(README.md|guides\/introduction\/.?)/, Topics: ~r/guides\/topics\/.?/ ] end + + defp groups_for_modules() do + # Ungrouped Modules: + # + # ElvenGard.Network + # ElvenGard.Network.Socket + # ElvenGard.Network.Type + # ElvenGard.Network.View + + [ + Endpoint: [ + ElvenGard.Network.Endpoint, + ElvenGard.Network.Endpoint.Protocol, + ElvenGard.Network.NetworkCodec, + ElvenGard.Network.PacketSerializer + ] + ] + end end From 6982ab3a5aa117d45fc37d12cc6140b87e7a1d7e Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 17:34:39 +0200 Subject: [PATCH 36/59] :memo: Update doc (3) --- README.md | 10 ++++----- TODOLIST.md | 1 + lib/elven_gard/network.ex | 2 ++ lib/elven_gard/network/endpoint/protocol.ex | 15 +++++++++---- lib/elven_gard/network/network_codec.ex | 14 ++++++------ lib/elven_gard/network/packet_serializer.ex | 3 --- lib/elven_gard/network/type.ex | 14 ++++++------ lib/elven_gard/network/view.ex | 24 +++++++++++---------- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index cbb4c76..4ce9a65 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # ElvenGard.Network + + [![Build Status](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml/badge.svg?branch=master)](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml) [![Coverage Status](https://coveralls.io/repos/github/ImNotAVirus/elvengard_network/badge.svg?branch=master)](https://coveralls.io/github/ImNotAVirus/elvengard_network?branch=master) - - ## What is ElvenGard Currently, all independent developers wishing to create a Multiplayer game have already asked themselves the question of how to make the server part easily while being "solid" (minimum crash, latency, ...). Indeed, for the client part, there are many tools very well designed to realize it (Unity3D, Unreal Engine,...) but for the server part, each game being different there are currently very few solutions to do this work. -This is the goal of this project: make a toolkit to group together different functionalities present in any MMORPG (network part, quests, movements, objects in game, instances, etc...) to prepare bases for the developer so that he doesn't have to dwell on this part which is often tedious. +This is the goal of this ambitious project: make a toolkit to group together different functionalities present in any MMORPG (network part, quests, movements, objects in game, instances, etc...) to prepare bases for the developer so that he doesn't have to dwell on this part which is often tedious. ## What is ElvenGard.Network @@ -53,5 +53,5 @@ The docs can be found at [https://hexdocs.pm/elvengard_network](https://hexdocs. ## Contributing -Currently developing this project, I will often open pull-requests. Any review is welcome. -Also, feel free to fork the repository. +I'm currently developing this project. Any review or PR is welcome. +Also, feel free to fork the repository and contribute. diff --git a/TODOLIST.md b/TODOLIST.md index 5f13f20..9f58e0b 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -3,3 +3,4 @@ - Abstract the message handler and provide a generic way to handle all transport (ranch, gen_tcp, gen_udp, ...) - Refacto ElvenGard.Network.PacketSerializer - Refacto ElvenGard.Network.Endpoint.Protocol +- Document all `use` according to the bests practices diff --git a/lib/elven_gard/network.ex b/lib/elven_gard/network.ex index 4ec0665..34dc57a 100644 --- a/lib/elven_gard/network.ex +++ b/lib/elven_gard/network.ex @@ -1,4 +1,6 @@ defmodule ElvenGard.Network do + @external_resource "README.md" + @moduledoc "README.md" |> File.read!() |> String.split("") diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index 98b1308..4944e34 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -10,8 +10,8 @@ defmodule ElvenGard.Network.Endpoint.Protocol do a structured way to implement connection handling within ElvenGard.Network. For detailed information on implementing and using network protocols - with ElvenGard.Network, please refer to the [Endpoint Protocol guide] - (). + with ElvenGard.Network, please refer to the + [Endpoint Protocol guide](). """ alias ElvenGard.Network.Socket @@ -33,7 +33,15 @@ defmodule ElvenGard.Network.Endpoint.Protocol do @doc """ Callback called just after receiving a message. - For the return values, see `c:GenServer.handle_info/2` + This callback is invoked whenever a message is received on the connection. It should + return one of the following: + + - `:ignore`: the message received will not be decoded or processed by the protocol. + It will just be ignored + - `{:ignore, new_socket}`: same as `:ignore` but also modifies the socket + - `{:ok, new_socket}`: classic loop - decode the packet and process it + - `{:stop, reason, new_socket}`: stop the GenServer/Protocol and disconnect the client + """ @callback handle_message(message :: binary, socket :: Socket.t()) :: :ignore @@ -46,7 +54,6 @@ defmodule ElvenGard.Network.Endpoint.Protocol do Callback called after the socket connection is closed and before the GenServer shutdown. - For the return values, see `c:GenServer.handle_info/2` """ @callback handle_halt(reason :: term, socket :: Socket.t()) :: {:ok, new_socket} diff --git a/lib/elven_gard/network/network_codec.ex b/lib/elven_gard/network/network_codec.ex index e0681f4..1ec78b6 100644 --- a/lib/elven_gard/network/network_codec.ex +++ b/lib/elven_gard/network/network_codec.ex @@ -7,8 +7,7 @@ defmodule ElvenGard.Network.NetworkCodec do binary data for transmission over the network. For more information on how to implement this behavior and use packet encoding - and decoding, please refer to the [NetworkCodec guide] - (). + and decoding, please refer to the [NetworkCodec guide](). """ alias ElvenGard.Network.Socket @@ -23,7 +22,7 @@ defmodule ElvenGard.Network.NetworkCodec do ## Examples raw = <> - {packet1, remaining} = ElvenGard.Network.NetworkCodec.next(raw, socket) + {^packet1, ^remaining} = MyNetworkCodec.next(raw, socket) """ @callback next(raw :: bitstring, socket :: Socket.t()) :: @@ -39,13 +38,13 @@ defmodule ElvenGard.Network.NetworkCodec do ## Examples raw_packet = <<1::8, 123::8, "some data"::binary>> # Example raw packet data - %MessageStruct{id: 123, data: "some data"} = ElvenGard.Network.NetworkCodec.decode(raw_packet, socket) + %MessageStruct{id: 123, data: "some data"} = MyNetworkCodec.decode(raw_packet, socket) """ @callback decode(raw :: bitstring, socket :: Socket.t()) :: struct @doc """ - Encodes a packet for transmission. + Encodes a packet for network transmission. This callback function is responsible for encoding a structured packet or raw binary data into the binary format suitable for transmission over the network. @@ -55,9 +54,8 @@ defmodule ElvenGard.Network.NetworkCodec do ## Examples packet = %MessageStruct{id: 1, data: "Hello"} - <> = ElvenGard.Network.NetworkCodec.encode(packet, socket) + <> = MyNetworkCodec.encode(packet, socket) """ - @callback encode(packet | raw, socket :: Socket.t()) :: iodata() - when packet: struct(), raw: iodata() + @callback encode(packet :: struct() | iodata(), socket :: Socket.t()) :: iodata() end diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index ad617e7..d951d56 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -19,9 +19,6 @@ defmodule ElvenGard.Network.PacketSerializer do Users can specify guards in packet macros to conditionally match packets based on a condition (often using socket assigns). - For more details on using the packet macros, please refer to the guide at - . - ## Packet Structure, Serialization, and Deserialization The `defpacket` macros can generate a packet structure, a `deserialize/3` function for diff --git a/lib/elven_gard/network/type.ex b/lib/elven_gard/network/type.ex index 726b5bf..c9e4a65 100644 --- a/lib/elven_gard/network/type.ex +++ b/lib/elven_gard/network/type.ex @@ -7,15 +7,15 @@ defmodule ElvenGard.Network.Type do how a specific field type is decoded and encoded when parsing and generating packets. - To implement a custom type, you need to define the `decode/2` and `encode/2` - callbacks. The `decode/2` function takes a raw binary as input and decodes it + To implement a custom type, you need to define the `c:decode/2` and `c:encode/2` + callbacks. The `c:decode/2` function takes a raw binary as input and decodes it into a structured value of the custom type. The `encode/2` function takes a value of the custom type and encodes it into a binary representation. Note that the `ElvenGard.Network.PacketSerializer` module uses the callbacks in this - behaviour when decoding packet fields. For each field defined in a packet serializer, - the corresponding type's `decode/2` function will be called to parse the binary - data. + behaviour when encoding and decoding packet fields. For each field defined in a + packet serializer, the corresponding type's `c:encode/2` or `c:decode/2` function + will be called to parse the binary data. ## Example @@ -27,13 +27,13 @@ defmodule ElvenGard.Network.Type do @impl ElvenGard.Network.Type def decode(raw, _opts) do - <> = raw + <> = raw {value, rest} end @impl ElvenGard.Network.Type def encode(value, _opts) do - <> + <> end end diff --git a/lib/elven_gard/network/view.ex b/lib/elven_gard/network/view.ex index 6847a66..dbd6a3f 100644 --- a/lib/elven_gard/network/view.ex +++ b/lib/elven_gard/network/view.ex @@ -7,29 +7,31 @@ defmodule ElvenGard.Network.View do in this module, users can define how a specific packet view is rendered and converted into binary data for transmission. - To implement a custom view, you need to define the `render/2` callback. The - `render/2` function takes the name of the packet view and a map of parameters - as input and returns the binary data to be sent to the client. + To implement a custom view, you need to define the `c:render/2` callback. The + `c:render/2` function takes the name of the packet view and a map of parameters + as input and returns the binary data to be sent to the client or a structure + that will be serialized by `ElvenGard.Network.PacketSerializer`. ## Example Here's an example of defining a custom view for rendering a login response packet: - defmodule MyLoginResponseView do + defmodule MyApp.LoginViews do use ElvenGard.Network.View + + alias MyApp.Server.LoginPackets.LoginResponse @impl ElvenGard.Network.View def render(:login_response, %{status: status, message: message}) do - <> + # Encode the `json` field before generating the LoginResponse struct + json = Poison.encode!(message) + %LoginResponse{status: status, json: json} end end - In the above example, we defined a custom view module `MyLoginResponseView` that - implements the `render/2` callback for rendering a login response packet. - - Then, when sending a login response packet using `ElvenGard.Network.PacketView`, you - can use this custom view to render the packet before sending it to the client. + In the above example, we defined a custom views module `MyApp.LoginViews` that + implements the `c:render/2` callback for rendering a login response packet. """ alias ElvenGard.Network.UnknownViewError @@ -42,7 +44,7 @@ defmodule ElvenGard.Network.View do - `name`: A unique identifier packet view (string or atom). - `params`: A map or keyword list of parameters to be used when rendering the view. - The function should return an iodata to be sent to the client. + The function should return an iodata or a structure to be sent to the client. """ @callback render(name, params) :: iodata() | struct() when name: atom() | String.t(), params: map() | Keyword.t() From 7c25de0d0475231ed7e055159bbaaaba2ea402d9 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 18:39:39 +0200 Subject: [PATCH 37/59] :memo: Start guides --- CHANGELOG.md | 6 +++- TODOLIST.md | 1 + guides/introduction/endpoint.md | 1 + guides/introduction/example.md | 1 + guides/introduction/getting_started.md | 50 ++++++++++++++++++++++++++ guides/introduction/protocol.md | 1 + mix.exs | 14 ++++++-- 7 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 guides/introduction/endpoint.md create mode 100644 guides/introduction/example.md create mode 100644 guides/introduction/getting_started.md create mode 100644 guides/introduction/protocol.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4640904..0124cd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,5 @@ -# TODO +# Changelog + +## 0.1.0-alpha + +Pre-release, this is not a production ready release ! diff --git a/TODOLIST.md b/TODOLIST.md index 9f58e0b..39856d7 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -4,3 +4,4 @@ - Refacto ElvenGard.Network.PacketSerializer - Refacto ElvenGard.Network.Endpoint.Protocol - Document all `use` according to the bests practices +- Add telemetry diff --git a/guides/introduction/endpoint.md b/guides/introduction/endpoint.md new file mode 100644 index 0000000..63d2315 --- /dev/null +++ b/guides/introduction/endpoint.md @@ -0,0 +1 @@ +# Endpoint diff --git a/guides/introduction/example.md b/guides/introduction/example.md new file mode 100644 index 0000000..00887af --- /dev/null +++ b/guides/introduction/example.md @@ -0,0 +1 @@ +# Example: Login Server diff --git a/guides/introduction/getting_started.md b/guides/introduction/getting_started.md new file mode 100644 index 0000000..46a525d --- /dev/null +++ b/guides/introduction/getting_started.md @@ -0,0 +1,50 @@ +# Getting Started + +This guide is an introduction to [ElvenGard.Network](https://github.com/ImNotAVirus/elvengard_network), the Network toolkit for a MMO Game Server toolkit written in Elixir. +The purpose of this toolkit is to provide a set of modules and processes to simplify the creation of a game server. It's therefore simple to use and flexible in order to allow you to concentrate on the implementation of your features. + +## Adding ElvenGard.Network to an application + +To start off with, we'll generate a new Elixir application by running this command: + +``` +mix new login_server --sup +``` + +The `--sup` option ensures that this application has [a supervision tree](http://elixir-lang.org/getting-started/mix-otp/supervisor-and-application.html), which we'll need for ElvenGard a little later on. + +To add ElvenGard.Network to this application, just add an entry to your mix.exs: + +```elixir +defp deps do + [ + {:elvengard_network, github: "imnotavirus/elvengard_network"} + ] +end +``` + +To install this dependency, we will run this command: + +``` +mix deps.get +``` + +Then, we need to add a bit of configuration in `config/config.exs` + +```elixir +config :login_server, LoginServer.Endpoint, + listener_name: :login_server, + transport: :ranch_tcp, + transport_opts: [ip: "127.0.0.1", port: 3000], + protocol: LoginServer.Endpoint.Protocol + +config :login_server, LoginServer.Endpoint.Protocol, + packet_handler: LoginServer.Endpoint.PacketHandlers, + network_codec: LoginServer.Endpoint.NetworkCodec +``` + +For more details on the configuration of each module, please refer to guides [Endpoint](endpoint.html) and [Endpoint Protocol](protocol.html). + +## Summary + +At the end of this section, you must have setup your app to use Elvengard.Network. Now that we have everything installed, let's create our first Endpoint and get up and running. diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md new file mode 100644 index 0000000..5632042 --- /dev/null +++ b/guides/introduction/protocol.md @@ -0,0 +1 @@ +# Endpoint Protocol diff --git a/mix.exs b/mix.exs index 3414870..a2d246b 100644 --- a/mix.exs +++ b/mix.exs @@ -69,13 +69,21 @@ defmodule ElvenGard.Network.MixProject do end defp extras() do - ["README.md": [title: "Overview"]] ++ Path.wildcard("guides/**/*.md") + Enum.concat( + ["README.md": [title: "Overview"]], + [ + "CHANGELOG.md", + "guides/introduction/getting_started.md", + "guides/introduction/example.md", + "guides/introduction/endpoint.md", + "guides/introduction/protocol.md" + ] + ) end defp groups_for_extras() do [ - Introduction: ~r/(README.md|guides\/introduction\/.?)/, - Topics: ~r/guides\/topics\/.?/ + Introduction: ~r/(README.md|guides\/introduction\/.?)/ ] end From dd4c8f1abd6b6bfeafa22167bdccba34db887a7e Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 20:53:34 +0200 Subject: [PATCH 38/59] :memo: Add guide for Endpoints --- examples/login_server/.formatter.exs | 4 + examples/login_server/.gitignore | 2 + examples/login_server/config/config.exs | 11 +++ examples/login_server/lib/login_server.ex | 5 ++ .../lib/login_server/application.ex | 19 ++++ .../login_server/lib/login_server/endpoint.ex | 18 ++++ examples/login_server/mix.exs | 28 ++++++ examples/login_server/mix.lock | 3 + examples/login_server/test/test_helper.exs | 1 + guides/introduction/endpoint.md | 86 +++++++++++++++++++ guides/introduction/example.md | 1 - guides/introduction/getting_started.md | 11 ++- mix.exs | 1 - 13 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 examples/login_server/.formatter.exs create mode 100644 examples/login_server/.gitignore create mode 100644 examples/login_server/config/config.exs create mode 100644 examples/login_server/lib/login_server.ex create mode 100644 examples/login_server/lib/login_server/application.ex create mode 100644 examples/login_server/lib/login_server/endpoint.ex create mode 100644 examples/login_server/mix.exs create mode 100644 examples/login_server/mix.lock create mode 100644 examples/login_server/test/test_helper.exs delete mode 100644 guides/introduction/example.md diff --git a/examples/login_server/.formatter.exs b/examples/login_server/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/examples/login_server/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/examples/login_server/.gitignore b/examples/login_server/.gitignore new file mode 100644 index 0000000..2e39def --- /dev/null +++ b/examples/login_server/.gitignore @@ -0,0 +1,2 @@ +_build/ +deps/ diff --git a/examples/login_server/config/config.exs b/examples/login_server/config/config.exs new file mode 100644 index 0000000..04a7520 --- /dev/null +++ b/examples/login_server/config/config.exs @@ -0,0 +1,11 @@ +import Config + +config :login_server, LoginServer.Endpoint, + listener_name: :login_server, + transport: :ranch_tcp, + transport_opts: [ip: "127.0.0.1", port: 3000], + protocol: LoginServer.Endpoint.Protocol + +config :login_server, LoginServer.Endpoint.Protocol, + packet_handler: LoginServer.Endpoint.PacketHandlers, + network_codec: LoginServer.Endpoint.NetworkCodec diff --git a/examples/login_server/lib/login_server.ex b/examples/login_server/lib/login_server.ex new file mode 100644 index 0000000..8a1d081 --- /dev/null +++ b/examples/login_server/lib/login_server.ex @@ -0,0 +1,5 @@ +defmodule LoginServer do + @moduledoc """ + Documentation for `LoginServer`. + """ +end diff --git a/examples/login_server/lib/login_server/application.ex b/examples/login_server/lib/login_server/application.ex new file mode 100644 index 0000000..04f644c --- /dev/null +++ b/examples/login_server/lib/login_server/application.ex @@ -0,0 +1,19 @@ +defmodule LoginServer.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + LoginServer.Endpoint + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: LoginServer.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/examples/login_server/lib/login_server/endpoint.ex b/examples/login_server/lib/login_server/endpoint.ex new file mode 100644 index 0000000..c1cd441 --- /dev/null +++ b/examples/login_server/lib/login_server/endpoint.ex @@ -0,0 +1,18 @@ +defmodule LoginServer.Endpoint do + @moduledoc """ + Documentation for LoginServer.Endpoint + """ + + use ElvenGard.Network.Endpoint, otp_app: :login_server + + require Logger + + ## Callbacks + + @impl true + def handle_start(config) do + host = get_in(config, [:transport_opts, :socket_opts, :ip]) + port = get_in(config, [:transport_opts, :socket_opts, :port]) + Logger.info("LoginServer started on #{:inet.ntoa(host)}:#{port}") + end +end diff --git a/examples/login_server/mix.exs b/examples/login_server/mix.exs new file mode 100644 index 0000000..d594027 --- /dev/null +++ b/examples/login_server/mix.exs @@ -0,0 +1,28 @@ +defmodule LoginServer.MixProject do + use Mix.Project + + def project do + [ + app: :login_server, + version: "0.1.0", + elixir: "~> 1.13", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {LoginServer.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:elvengard_network, path: "../.."} + ] + end +end diff --git a/examples/login_server/mix.lock b/examples/login_server/mix.lock new file mode 100644 index 0000000..093d206 --- /dev/null +++ b/examples/login_server/mix.lock @@ -0,0 +1,3 @@ +%{ + "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, +} diff --git a/examples/login_server/test/test_helper.exs b/examples/login_server/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/examples/login_server/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/guides/introduction/endpoint.md b/guides/introduction/endpoint.md index 63d2315..010d2cd 100644 --- a/guides/introduction/endpoint.md +++ b/guides/introduction/endpoint.md @@ -1 +1,87 @@ # Endpoint + +The first thing to do is define a module using `ElvenGard.Network.Endpoint`. + +An Endpoint is simply a [Ranch listener](https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/). + +## Configuration + +Configuration is performed via the `config/config.exs` file as you did previously. + +```elixir +config :login_server, LoginServer.Endpoint, + listener_name: :login_server, + transport: :ranch_tcp, + transport_opts: [ip: "127.0.0.1", port: 3000], + protocol: LoginServer.Endpoint.Protocol +``` + +Here's what each configuration option does: + + - `listener_name: :login_server`: specifies a unique name for the listener. This is used by Ranch + to manage the listener process. + - `transport: :ranch_tcp`: specifies the transport protocol to use. In this case, it's Ranch's TCP + transport. + - `transport_opts: [ip: "127.0.0.1", port: 3000]`: provides options for configuring the transport. + In this case, it specifies the IP address and port on which the server will listen for incoming + connections. + - `protocol: LoginServer.Endpoint.Protocol`: sets the protocol module that will handle client + connections and communication. Here, it references the `LoginServer.Endpoint.Protocol` module. + +## Creating an endpoint + +Here's a basic example you can use for all your projects: + +```elixir +# file: lib/login_server/endpoint.ex +defmodule LoginServer.Endpoint do + @moduledoc """ + Documentation for LoginServer.Endpoint + """ + + use ElvenGard.Network.Endpoint, otp_app: :login_server + + require Logger + + ## Callbacks + + @impl true + def handle_start(config) do + host = get_in(config, [:transport_opts, :socket_opts, :ip]) + port = get_in(config, [:transport_opts, :socket_opts, :port]) + Logger.info("LoginServer started on #{:inet.ntoa(host)}:#{port}") + end +end +``` + +As you can see, creating an endpoint is very simple, you just need to specify the +otp app you used in the config and you're done. + +It is also possible to define the `c:ElvenGard.Network.Endpoint.handle_start/1` +callback. This allows you to, for example, display various information relating +to the startup. It receives the endpoint configs as a parameter and must always +return `:ok`. + +## Add to supervision tree + +Once you've created and configured your endpoint, all you have to do is place it +in your application's supervision tree. + +```elixir +# file: lib/login_server/application.ex + + ... + + def start(_type, _args) do + children = [ + LoginServer.Endpoint + ] + + ... +``` + +## Summary + +At the end of this part, you should have a working Endpoint listening on port 3000 +(see config file). +Now it's time to create a Protocol to receive our first packets. diff --git a/guides/introduction/example.md b/guides/introduction/example.md deleted file mode 100644 index 00887af..0000000 --- a/guides/introduction/example.md +++ /dev/null @@ -1 +0,0 @@ -# Example: Login Server diff --git a/guides/introduction/getting_started.md b/guides/introduction/getting_started.md index 46a525d..6cfbd69 100644 --- a/guides/introduction/getting_started.md +++ b/guides/introduction/getting_started.md @@ -3,6 +3,9 @@ This guide is an introduction to [ElvenGard.Network](https://github.com/ImNotAVirus/elvengard_network), the Network toolkit for a MMO Game Server toolkit written in Elixir. The purpose of this toolkit is to provide a set of modules and processes to simplify the creation of a game server. It's therefore simple to use and flexible in order to allow you to concentrate on the implementation of your features. +In this guide, we're going to learn some basics about ElvenGard.Network. If you want +to see the code from this guide, you can view it at [elvengard_network/examples/login_server on GitHub](https://github.com/ImNotAVirus/elvengard_network/tree/master/examples/login_server). + ## Adding ElvenGard.Network to an application To start off with, we'll generate a new Elixir application by running this command: @@ -18,7 +21,7 @@ To add ElvenGard.Network to this application, just add an entry to your mix.exs: ```elixir defp deps do [ - {:elvengard_network, github: "imnotavirus/elvengard_network"} + {:elvengard_network, "~> 0.1"} ] end ``` @@ -32,6 +35,8 @@ mix deps.get Then, we need to add a bit of configuration in `config/config.exs` ```elixir +import Config + config :login_server, LoginServer.Endpoint, listener_name: :login_server, transport: :ranch_tcp, @@ -43,7 +48,9 @@ config :login_server, LoginServer.Endpoint.Protocol, network_codec: LoginServer.Endpoint.NetworkCodec ``` -For more details on the configuration of each module, please refer to guides [Endpoint](endpoint.html) and [Endpoint Protocol](protocol.html). +This configuration example simply tells our endpoint to listen on port 3000 of the local address and defines the protocol, network encoder and packet handlers to be used. + +For more details on the configuration of each module, please refer to [Endpoint](endpoint.html#configuration) and [Protocol](protocol.html#configuration) guides and [Ranch documentation](https://ninenines.eu/docs/en/ranch/2.1/guide/). ## Summary diff --git a/mix.exs b/mix.exs index a2d246b..3e68738 100644 --- a/mix.exs +++ b/mix.exs @@ -74,7 +74,6 @@ defmodule ElvenGard.Network.MixProject do [ "CHANGELOG.md", "guides/introduction/getting_started.md", - "guides/introduction/example.md", "guides/introduction/endpoint.md", "guides/introduction/protocol.md" ] From 9eceb992b511c2df5503a0a36ef6520b1be1d5f6 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 21:34:23 +0200 Subject: [PATCH 39/59] :memo: Add guide for Protocols --- .../lib/login_server/endpoint/protocol.ex | 35 ++++++++ guides/introduction/endpoint.md | 4 +- guides/introduction/protocol.md | 89 ++++++++++++++++++- lib/elven_gard/network/endpoint.ex | 2 +- lib/elven_gard/network/endpoint/protocol.ex | 2 +- lib/elven_gard/network/socket.ex | 7 ++ 6 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 examples/login_server/lib/login_server/endpoint/protocol.ex diff --git a/examples/login_server/lib/login_server/endpoint/protocol.ex b/examples/login_server/lib/login_server/endpoint/protocol.ex new file mode 100644 index 0000000..b779ef6 --- /dev/null +++ b/examples/login_server/lib/login_server/endpoint/protocol.ex @@ -0,0 +1,35 @@ +defmodule LoginServer.Endpoint.Protocol do + @moduledoc """ + Documentation for LoginServer.Endpoint.Protocol + """ + + use ElvenGard.Network.Endpoint.Protocol + + require Logger + + alias ElvenGard.Network.Socket + + ## Callbacks + + @impl true + def handle_init(%Socket{} = socket) do + Logger.info("New connection: #{socket.id}") + + %Socket{transport: transport, transport_pid: transport_pid} = socket + :ok = transport.setopts(transport_pid, packet: :line, reuseaddr: true) + + {:ok, socket} + end + + @impl true + def handle_message(message, %Socket{} = socket) do + Logger.debug("New message from #{socket.id}: #{inspect(message)}") + :ignore + end + + @impl true + def handle_halt(reason, %Socket{} = socket) do + Logger.info("#{socket.id} is now disconnected (reason: #{inspect(reason)})") + {:ok, socket} + end +end diff --git a/guides/introduction/endpoint.md b/guides/introduction/endpoint.md index 010d2cd..42cd278 100644 --- a/guides/introduction/endpoint.md +++ b/guides/introduction/endpoint.md @@ -26,9 +26,9 @@ Here's what each configuration option does: In this case, it specifies the IP address and port on which the server will listen for incoming connections. - `protocol: LoginServer.Endpoint.Protocol`: sets the protocol module that will handle client - connections and communication. Here, it references the `LoginServer.Endpoint.Protocol` module. + connections and communication. -## Creating an endpoint +## Creating an Endpoint Here's a basic example you can use for all your projects: diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md index 5632042..1a0eef9 100644 --- a/guides/introduction/protocol.md +++ b/guides/introduction/protocol.md @@ -1 +1,88 @@ -# Endpoint Protocol +# Protocol + +In this section, we will lear how to use `ElvenGard.Network.Endpoint.Protocol`. + +An Protocol is wrapper around [Ranch protocols](https://ninenines.eu/docs/en/ranch/2.1/guide/protocols/). + +## Configuration + +Configuration is performed via the `config/config.exs` file as you did previously. + +```elixir +config :login_server, LoginServer.Endpoint.Protocol, + packet_handler: LoginServer.Endpoint.PacketHandlers, + network_codec: LoginServer.Endpoint.NetworkCodec +``` + +Here's the explanation for these options: + + - `packet_handler: LoginServer.PacketHandler`: specifies the module responsible for + handling packets received from clients. + - `network_codec: LoginServer.NetworkCodec`: Specifies the module responsible for + encoding and decoding packets for communication between clients and the server. + +## Creating a Protocol + +For this part, we're going to create a fairly simple protocol that will just display +our packets. + +```elixir +# file: lib/endpoint/protocol.ex +defmodule LoginServer.Endpoint.Protocol do + @moduledoc """ + Documentation for LoginServer.Endpoint.Protocol + """ + + use ElvenGard.Network.Endpoint.Protocol + + require Logger + + alias ElvenGard.Network.Socket + + ## Callbacks + + @impl true + def handle_init(%Socket{} = socket) do + Logger.info("New connection: #{socket.id}") + + %Socket{transport: transport, transport_pid: transport_pid} = socket + :ok = transport.setopts(transport_pid, packet: :line, reuseaddr: true) + + {:ok, socket} + end + + @impl true + def handle_message(message, %Socket{} = socket) do + Logger.debug("New message from #{socket.id}: #{inspect(message)}") + :ignore + end + + @impl true + def handle_halt(reason, %Socket{} = socket) do + Logger.info("#{socket.id} is now disconnected (reason: #{inspect(reason)})") + {:ok, socket} + end +end +``` + +Once again, creating a Protocol is fairly straightforward. + +This example defines 3 callbacks : + + - `handle_init/1`: called when a client connects, it is mainly used to set + [socket options](https://www.erlang.org/doc/man/inet#setopts-2) or + call `ElvenGard.Network.Socket.assign/2` to init assigns. + - `handle_message/2`: called when we receive a packet from a client, we can + either ignore it by returning `:ignore`, or choose to decode it and then + handle it by returning `:ok`. + - `handle_halt/2`: called when a client disconnects + +## Summary + +If you run your application in its current state, you'll see that it's possible +to connect to our server and send it messages, and that these are displayed by +our application. +Since we're using a text-based protocol, you can use Netcat as client for example. + +Now that packets can be received, they need to be decoded and processed. +This is what we'll see next. diff --git a/lib/elven_gard/network/endpoint.ex b/lib/elven_gard/network/endpoint.ex index ddf4ed7..a5e0262 100644 --- a/lib/elven_gard/network/endpoint.ex +++ b/lib/elven_gard/network/endpoint.ex @@ -1,6 +1,6 @@ defmodule ElvenGard.Network.Endpoint do @moduledoc ~S""" - Wrapper on top of Ranch's [listeners](https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/). + Wrapper on top of [Ranch listeners](https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/). This module provides a wrapper around the Ranch library to define network endpoints. Endpoints are crucial for managing incoming connections and diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index 4944e34..e44b42d 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -1,6 +1,6 @@ defmodule ElvenGard.Network.Endpoint.Protocol do @moduledoc ~S""" - Wrapper on top of Ranch's [protocols](https://ninenines.eu/docs/en/ranch/2.1/guide/protocols/). + Wrapper on top of [Ranch protocols](https://ninenines.eu/docs/en/ranch/2.1/guide/protocols/). This module defines a protocol behavior to handle incoming connections in the ElvenGard.Network library. It provides callbacks for initializing, handling diff --git a/lib/elven_gard/network/socket.ex b/lib/elven_gard/network/socket.ex index b09ba62..d832c80 100644 --- a/lib/elven_gard/network/socket.ex +++ b/lib/elven_gard/network/socket.ex @@ -91,7 +91,14 @@ defmodule ElvenGard.Network.Socket do ## Examples iex> assign(socket, :name, "ElvenGard") + iex> socket.assigns.name == "ElvenGard" + true + iex> assign(socket, name: "ElvenGard", logo: "🌸") + iex> socket.assigns.name == "ElvenGard" + true + iex> socket.assigns.logo == "🌸" + true """ @spec assign(Socket.t(), atom, any) :: Socket.t() def assign(%Socket{} = socket, key, value) do From 4116f99599af8fd2e0c16aaa6c95ae7b0194f709 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 15 Aug 2023 22:52:46 +0200 Subject: [PATCH 40/59] :memo: Add guide for Network Protocol --- guides/introduction/network_protocol.md | 73 +++++++++++++++++++++++++ mix.exs | 1 + 2 files changed, 74 insertions(+) create mode 100644 guides/introduction/network_protocol.md diff --git a/guides/introduction/network_protocol.md b/guides/introduction/network_protocol.md new file mode 100644 index 0000000..39e529e --- /dev/null +++ b/guides/introduction/network_protocol.md @@ -0,0 +1,73 @@ +# Network Protocol + +Before we start coding, we need to define our network protocol, i.e. what our packets +are going to look like, how they are going to be encoded, decoded and so on. + +For this guide, we're going to use a **text-based protocol**, meaning that all commands +exchanged are text that can be read by a human. +We'll be using the `String` module for parsing and tools such as Netcat as a client. + +## Packets + +In this section we'll look at the different packets and interactions between client +and server. + +### Ping + +A client can send a `PingRequest`: + +| Packet ID | Field Name | Field Type | Notes | +|-----------|------------|------------|-------| +| PING | no fields | | | + +When the server receives this packet, it must respond with a `PongResponse`: + +| Packet ID | Field Name | Field Type | Notes | +|-----------|------------|------------|-----------------------------------------------------| +| PONG | time | DateTime | Time at which the server received the ping requests | + +### Login + +A client can send a `LoginRequest`: + +| Packet ID | Field Name | Field Type | Notes | +|-----------|------------|------------|-------| +| LOGIN | username | String | | +| | password | String | | + +Depending on whether the identifiers are correct or not, the server can respond in 2 ways, +`LoginFailed`: + +| Packet ID | Field Name | Field Type | Notes | +|-----------|------------|------------|---------------------------------------| +| FAIL | reason | String | The reason why the user cannot log in | + +or `LoginSucceed`: + +| Packet ID | Field Name | Field Type | Notes | +|-----------|------------|------------|---------------------------| +| SUCCESS | world_info | WorldInfo | Host and port of our game | + +## Network encoding/decoding + +Now that we know what our packets will look like, we need to define how we're going to +exchange them on the network. + +As Elvengard.Network is currently based on +[Ranch](https://ninenines.eu/docs/en/ranch/2.1/guide/), it only supports the **TCP protocol**. +Furthermore, using a text protocol, we'll just separate our different packages by +**line breaks**. + +Here's an example of what each packet might look like: + + - PingRequest: `PING` + - PongResponse: `PONG 2023-08-15 20:45:07.068297Z` + - LoginRequest: `LOGIN admin password` + - LoginFailed: `FAIL Bad credentials :/` + - LoginSucceed: `SUCCESS 127.0.0.1:5000` + +## Summary + +Now that you know how your client and server will communicate, you can create and +configure your project. +That's what we'll look at in the next section. diff --git a/mix.exs b/mix.exs index 3e68738..b1fc033 100644 --- a/mix.exs +++ b/mix.exs @@ -73,6 +73,7 @@ defmodule ElvenGard.Network.MixProject do ["README.md": [title: "Overview"]], [ "CHANGELOG.md", + "guides/introduction/network_protocol.md", "guides/introduction/getting_started.md", "guides/introduction/endpoint.md", "guides/introduction/protocol.md" From 2c817869825681dc0b0134650dd034bd161f8cd1 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 00:57:33 +0200 Subject: [PATCH 41/59] :tada: End of guide example --- TODOLIST.md | 1 + examples/login_server/.formatter.exs | 3 +- .../lib/login_server/client_packets.ex | 22 ++++++++++ .../login_server/endpoint/network_codec.ex | 43 +++++++++++++++++++ .../login_server/endpoint/packet_handlers.ex | 43 +++++++++++++++++++ .../lib/login_server/endpoint/protocol.ex | 2 +- .../lib/login_server/packet_views.ex | 23 ++++++++++ .../lib/login_server/server_packets.ex | 29 +++++++++++++ .../login_server/sub_packets/world_info.ex | 32 ++++++++++++++ .../lib/login_server/types/date_time_type.ex | 20 +++++++++ .../lib/login_server/types/integer_type.ex | 20 +++++++++ .../lib/login_server/types/string_type.ex | 26 +++++++++++ guides/introduction/protocol.md | 9 +++- 13 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 examples/login_server/lib/login_server/client_packets.ex create mode 100644 examples/login_server/lib/login_server/endpoint/network_codec.ex create mode 100644 examples/login_server/lib/login_server/endpoint/packet_handlers.ex create mode 100644 examples/login_server/lib/login_server/packet_views.ex create mode 100644 examples/login_server/lib/login_server/server_packets.ex create mode 100644 examples/login_server/lib/login_server/sub_packets/world_info.ex create mode 100644 examples/login_server/lib/login_server/types/date_time_type.ex create mode 100644 examples/login_server/lib/login_server/types/integer_type.ex create mode 100644 examples/login_server/lib/login_server/types/string_type.ex diff --git a/TODOLIST.md b/TODOLIST.md index 39856d7..04b9118 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -5,3 +5,4 @@ - Refacto ElvenGard.Network.Endpoint.Protocol - Document all `use` according to the bests practices - Add telemetry +- PacketHandlers behaviour diff --git a/examples/login_server/.formatter.exs b/examples/login_server/.formatter.exs index d2cda26..37b3bce 100644 --- a/examples/login_server/.formatter.exs +++ b/examples/login_server/.formatter.exs @@ -1,4 +1,5 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + import_deps: [:elvengard_network] ] diff --git a/examples/login_server/lib/login_server/client_packets.ex b/examples/login_server/lib/login_server/client_packets.ex new file mode 100644 index 0000000..2b1f486 --- /dev/null +++ b/examples/login_server/lib/login_server/client_packets.ex @@ -0,0 +1,22 @@ +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 diff --git a/examples/login_server/lib/login_server/endpoint/network_codec.ex b/examples/login_server/lib/login_server/endpoint/network_codec.ex new file mode 100644 index 0000000..84607b0 --- /dev/null +++ b/examples/login_server/lib/login_server/endpoint/network_codec.ex @@ -0,0 +1,43 @@ +defmodule LoginServer.Endpoint.NetworkCodec do + @moduledoc """ + Documentation for LoginServer.Endpoint.NetworkCodec + """ + + @behaviour ElvenGard.Network.NetworkCodec + + alias LoginServer.ClientPackets + + ## Behaviour impls + + @impl true + def next(raw, _socket) do + case String.split(raw, "\n", parts: 2) do + [packet] -> {packet, ""} + [packet, rest] -> {packet, rest} + end + end + + @impl true + def decode(raw, socket) do + case String.split(raw, " ", parts: 2) do + [packet_id] -> ClientPackets.deserialize(packet_id, "", socket) + [packet_id, params] -> ClientPackets.deserialize(packet_id, params, socket) + end + end + + @impl true + def encode(struct, _socket) when is_struct(struct) do + # %LoginSucceed{world_info: %WorldInfo{host: "127.0.0.1", port: 5000}} + struct + # {"SUCCESS", ["127.0.0.1", ":", "5000"]} + |> struct.__struct__.serialize() + # ["SUCCESS", ["127.0.0.1", ":", "5000"]] + |> Tuple.to_list() + # ["SUCCESS", " ", ["127.0.0.1", ":", "5000"]] + |> Enum.intersperse(" ") + # [["SUCCESS", " ", ["127.0.0.1", ":", "5000"]], "\n"] + |> then(&[&1 | "\n"]) + + # This will be serialized into `SUCCESS 127.0.0.1:5000\n` + end +end diff --git a/examples/login_server/lib/login_server/endpoint/packet_handlers.ex b/examples/login_server/lib/login_server/endpoint/packet_handlers.ex new file mode 100644 index 0000000..f2f585c --- /dev/null +++ b/examples/login_server/lib/login_server/endpoint/packet_handlers.ex @@ -0,0 +1,43 @@ +defmodule LoginServer.Endpoint.PacketHandlers do + @moduledoc """ + LoginServer.Endpoint.PacketHandler + """ + + alias ElvenGard.Network.Socket + + alias LoginServer.ClientPackets.{PingRequest, LoginRequest} + alias LoginServer.PacketViews + + ## Handlers + + def handle_packet(%PingRequest{}, socket) do + render = PacketViews.render(:pong_response, %{time: DateTime.utc_now()}) + :ok = Socket.send(socket, render) + {:cont, socket} + end + + def handle_packet(%LoginRequest{username: username, password: password}, socket) do + render = + if auth_using_db(username, password) do + PacketViews.render(:login_succeed, %{world: get_worlds_from_manager()}) + else + PacketViews.render(:login_failed, %{reason: "Bad credentials :/`"}) + end + + :ok = Socket.send(socket, render) + {:halt, socket} + end + + ## Fake functions + + defp auth_using_db(username, password) do + case {username, password} do + {"admin", "password"} -> true + _ -> false + end + end + + defp get_worlds_from_manager() do + %{host: "127.0.0.1", port: 5000} + end +end diff --git a/examples/login_server/lib/login_server/endpoint/protocol.ex b/examples/login_server/lib/login_server/endpoint/protocol.ex index b779ef6..a1c9356 100644 --- a/examples/login_server/lib/login_server/endpoint/protocol.ex +++ b/examples/login_server/lib/login_server/endpoint/protocol.ex @@ -24,7 +24,7 @@ defmodule LoginServer.Endpoint.Protocol do @impl true def handle_message(message, %Socket{} = socket) do Logger.debug("New message from #{socket.id}: #{inspect(message)}") - :ignore + {:ok, socket} end @impl true diff --git a/examples/login_server/lib/login_server/packet_views.ex b/examples/login_server/lib/login_server/packet_views.ex new file mode 100644 index 0000000..ebf49da --- /dev/null +++ b/examples/login_server/lib/login_server/packet_views.ex @@ -0,0 +1,23 @@ +defmodule LoginServer.PacketViews do + @moduledoc """ + Documentation for LoginServer.PacketViews + """ + + use ElvenGard.Network.View + + alias LoginServer.ServerPackets.{PongResponse, LoginFailed, LoginSucceed} + alias LoginServer.SubPackets.WorldInfo + + @impl true + def render(:pong_response, %{time: time}) do + %PongResponse{time: time} + end + + def render(:login_failed, %{reason: reason}) do + %LoginFailed{reason: reason} + end + + def render(:login_succeed, %{world: world}) do + %LoginSucceed{world: %WorldInfo{host: world.host, port: world.port}} + end +end diff --git a/examples/login_server/lib/login_server/server_packets.ex b/examples/login_server/lib/login_server/server_packets.ex new file mode 100644 index 0000000..f4060dd --- /dev/null +++ b/examples/login_server/lib/login_server/server_packets.ex @@ -0,0 +1,29 @@ +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 diff --git a/examples/login_server/lib/login_server/sub_packets/world_info.ex b/examples/login_server/lib/login_server/sub_packets/world_info.ex new file mode 100644 index 0000000..78e769d --- /dev/null +++ b/examples/login_server/lib/login_server/sub_packets/world_info.ex @@ -0,0 +1,32 @@ +defmodule LoginServer.SubPackets.WorldInfo do + @moduledoc """ + Documentation for LoginServer.SubPackets.WorldInfo + """ + + use ElvenGard.Network.Type + + alias __MODULE__ + alias LoginServer.Types.{IntegerType, StringType} + + @enforce_keys [:host, :port] + defstruct [:host, :port] + + @type t :: %WorldInfo{host: StringType.t(), port: IntegerType.t()} + + ## Behaviour impls + + @impl true + def decode(_data, _opts), do: raise("unimplemented") + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, opts) when is_struct(data, WorldInfo) do + separator = Keyword.fetch!(opts, :sep) + + [ + StringType.encode(data.host), + separator, + IntegerType.encode(data.port) + ] + end +end diff --git a/examples/login_server/lib/login_server/types/date_time_type.ex b/examples/login_server/lib/login_server/types/date_time_type.ex new file mode 100644 index 0000000..d917608 --- /dev/null +++ b/examples/login_server/lib/login_server/types/date_time_type.ex @@ -0,0 +1,20 @@ +defmodule LoginServer.Types.DateTimeType do + @moduledoc """ + Documentation for LoginServer.Types.DateTimeType + """ + + use ElvenGard.Network.Type + + @type t :: DateTime.t() + + ## Behaviour impls + + @impl true + def decode(_data, _opts), do: raise("unimplemented") + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, _opts) when is_struct(data, DateTime) do + DateTime.to_string(data) + end +end diff --git a/examples/login_server/lib/login_server/types/integer_type.ex b/examples/login_server/lib/login_server/types/integer_type.ex new file mode 100644 index 0000000..98051bd --- /dev/null +++ b/examples/login_server/lib/login_server/types/integer_type.ex @@ -0,0 +1,20 @@ +defmodule LoginServer.Types.IntegerType do + @moduledoc """ + Documentation for LoginServer.Types.IntegerType + """ + + use ElvenGard.Network.Type + + @type t :: integer() + + ## Behaviour impls + + @impl true + def decode(_data, _opts), do: raise("unimplemented") + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, _opts) when is_integer(data) do + Integer.to_string(data) + end +end diff --git a/examples/login_server/lib/login_server/types/string_type.ex b/examples/login_server/lib/login_server/types/string_type.ex new file mode 100644 index 0000000..b8037bc --- /dev/null +++ b/examples/login_server/lib/login_server/types/string_type.ex @@ -0,0 +1,26 @@ +defmodule LoginServer.Types.StringType do + @moduledoc """ + Documentation for LoginServer.Types.StringType + """ + + use ElvenGard.Network.Type + + @type t :: String.t() + + ## Behaviour impls + + @impl true + @spec decode(binary(), Keyword.t()) :: {t(), binary()} + def decode(data, _opts) when is_binary(data) do + case String.split(data, " ", parts: 2) do + [string] -> {string, ""} + [string, rest] -> {string, rest} + end + end + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, _opts) when is_binary(data) do + data + end +end diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md index 1a0eef9..9c40c9c 100644 --- a/guides/introduction/protocol.md +++ b/guides/introduction/protocol.md @@ -1,6 +1,6 @@ # Protocol -In this section, we will lear how to use `ElvenGard.Network.Endpoint.Protocol`. +In this section, we will learn how to use `ElvenGard.Network.Endpoint.Protocol`. An Protocol is wrapper around [Ranch protocols](https://ninenines.eu/docs/en/ranch/2.1/guide/protocols/). @@ -77,6 +77,13 @@ This example defines 3 callbacks : handle it by returning `:ok`. - `handle_halt/2`: called when a client disconnects +**Note**: you may notice that we define the `packet: :line` option in `handle_init/1`. +We use this option because we want to use a line break as a separator for our packets. +This works because, according to our [network protocol](network_protocol.html), we use +a text protocol where each packet is separated by a `\n`. However, for a binary protocol, +you may need to use `packet: :raw` or other options. +For more information on available options, see `:inet.setopts/2`. + ## Summary If you run your application in its current state, you'll see that it's possible From a04b7c38de6e833cddb21490fc02d24dddce2694 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 01:07:07 +0200 Subject: [PATCH 42/59] :memo: WIP guide for Types and Subpackets --- guides/introduction/types_and_subpackets.md | 9 +++++++++ mix.exs | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 guides/introduction/types_and_subpackets.md diff --git a/guides/introduction/types_and_subpackets.md b/guides/introduction/types_and_subpackets.md new file mode 100644 index 0000000..6631a94 --- /dev/null +++ b/guides/introduction/types_and_subpackets.md @@ -0,0 +1,9 @@ +# Types and Subpackets + +In this section, we will learn how to use `ElvenGard.Network.Type`. + +We're going to create all the types and subpackets needed to serialize and deserialize +our packets according to our [network protocol](network_protocol.html). + + +## Types diff --git a/mix.exs b/mix.exs index b1fc033..0bd166a 100644 --- a/mix.exs +++ b/mix.exs @@ -76,7 +76,8 @@ defmodule ElvenGard.Network.MixProject do "guides/introduction/network_protocol.md", "guides/introduction/getting_started.md", "guides/introduction/endpoint.md", - "guides/introduction/protocol.md" + "guides/introduction/protocol.md", + "guides/introduction/types_and_subpackets.md" ] ) end From 92075116115a08ff863786159481cd45352c46fb Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 10:04:32 +0200 Subject: [PATCH 43/59] :memo: Update project versions in doc --- README.md | 2 +- guides/introduction/getting_started.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ce9a65..733d067 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The package can be installed by adding `elvengard_network` to your list of depen ```elixir def deps do [ - {:elvengard_network, "~> 0.1"} + {:elvengard_network, "~> 0.1.0-alpha"} ] end ``` diff --git a/guides/introduction/getting_started.md b/guides/introduction/getting_started.md index 6cfbd69..79a2d29 100644 --- a/guides/introduction/getting_started.md +++ b/guides/introduction/getting_started.md @@ -21,7 +21,7 @@ To add ElvenGard.Network to this application, just add an entry to your mix.exs: ```elixir defp deps do [ - {:elvengard_network, "~> 0.1"} + {:elvengard_network, "~> 0.1.0-alpha"} ] end ``` From a77e50ff0ba0a68439ca7b180162bcb14033bed1 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 10:13:33 +0200 Subject: [PATCH 44/59] :memo: Typo --- guides/introduction/network_protocol.md | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/guides/introduction/network_protocol.md b/guides/introduction/network_protocol.md index 39e529e..c263c96 100644 --- a/guides/introduction/network_protocol.md +++ b/guides/introduction/network_protocol.md @@ -35,7 +35,7 @@ A client can send a `LoginRequest`: | LOGIN | username | String | | | | password | String | | -Depending on whether the identifiers are correct or not, the server can respond in 2 ways, +Depending on whether the credentials are correct or not, the server can respond in 2 ways, `LoginFailed`: | Packet ID | Field Name | Field Type | Notes | @@ -44,9 +44,9 @@ Depending on whether the identifiers are correct or not, the server can respond or `LoginSucceed`: -| Packet ID | Field Name | Field Type | Notes | -|-----------|------------|------------|---------------------------| -| SUCCESS | world_info | WorldInfo | Host and port of our game | +| Packet ID | Field Name | Field Type | Notes | +|-----------|------------|------------|----------------------------------| +| SUCCESS | world_info | WorldInfo | Host and port of our game server | ## Network encoding/decoding @@ -54,17 +54,17 @@ Now that we know what our packets will look like, we need to define how we're go exchange them on the network. As Elvengard.Network is currently based on -[Ranch](https://ninenines.eu/docs/en/ranch/2.1/guide/), it only supports the **TCP protocol**. -Furthermore, using a text protocol, we'll just separate our different packages by -**line breaks**. +[Ranch](https://ninenines.eu/docs/en/ranch/2.1/guide/), it only supports the **TCP protocol**. +And, as we use a text protocol, we'll just separate our different packets by +**line breaks** (`\n`). -Here's an example of what each packet might look like: +Here's an example of what each packet might look like once encoded: - - PingRequest: `PING` - - PongResponse: `PONG 2023-08-15 20:45:07.068297Z` - - LoginRequest: `LOGIN admin password` - - LoginFailed: `FAIL Bad credentials :/` - - LoginSucceed: `SUCCESS 127.0.0.1:5000` + - PingRequest: `PING\n` + - PongResponse: `PONG 2023-08-15 20:45:07.068297Z\n` + - LoginRequest: `LOGIN admin password\n` + - LoginFailed: `FAIL Bad credentials\n` + - LoginSucceed: `SUCCESS 127.0.0.1:5000\n` ## Summary From d9acf7497b590abc366d33dedf3695d6a77d9ea3 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:29:01 +0200 Subject: [PATCH 45/59] :memo: Add guide for Types and Subpackets --- .../login_server/sub_packets/world_info.ex | 2 +- guides/introduction/network_protocol.md | 4 +- guides/introduction/protocol.md | 4 +- guides/introduction/types_and_subpackets.md | 165 +++++++++++++++++- 4 files changed, 168 insertions(+), 7 deletions(-) diff --git a/examples/login_server/lib/login_server/sub_packets/world_info.ex b/examples/login_server/lib/login_server/sub_packets/world_info.ex index 78e769d..188d86f 100644 --- a/examples/login_server/lib/login_server/sub_packets/world_info.ex +++ b/examples/login_server/lib/login_server/sub_packets/world_info.ex @@ -25,7 +25,7 @@ defmodule LoginServer.SubPackets.WorldInfo do [ StringType.encode(data.host), - separator, + StringType.encode(separator), IntegerType.encode(data.port) ] end diff --git a/guides/introduction/network_protocol.md b/guides/introduction/network_protocol.md index c263c96..9bc64f3 100644 --- a/guides/introduction/network_protocol.md +++ b/guides/introduction/network_protocol.md @@ -55,8 +55,8 @@ exchange them on the network. As Elvengard.Network is currently based on [Ranch](https://ninenines.eu/docs/en/ranch/2.1/guide/), it only supports the **TCP protocol**. -And, as we use a text protocol, we'll just separate our different packets by -**line breaks** (`\n`). +And, as we use a text protocol, we'll just separate our fields with a **space** and our +different packets with a **line breaks** (`\n`). Here's an example of what each packet might look like once encoded: diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md index 9c40c9c..a098c8a 100644 --- a/guides/introduction/protocol.md +++ b/guides/introduction/protocol.md @@ -75,9 +75,9 @@ This example defines 3 callbacks : - `handle_message/2`: called when we receive a packet from a client, we can either ignore it by returning `:ignore`, or choose to decode it and then handle it by returning `:ok`. - - `handle_halt/2`: called when a client disconnects + - `handle_halt/2`: called when a client disconnects. -**Note**: you may notice that we define the `packet: :line` option in `handle_init/1`. +**NOTE**: you may notice that we define the `packet: :line` option in `handle_init/1`. We use this option because we want to use a line break as a separator for our packets. This works because, according to our [network protocol](network_protocol.html), we use a text protocol where each packet is separated by a `\n`. However, for a binary protocol, diff --git a/guides/introduction/types_and_subpackets.md b/guides/introduction/types_and_subpackets.md index 6631a94..484261b 100644 --- a/guides/introduction/types_and_subpackets.md +++ b/guides/introduction/types_and_subpackets.md @@ -1,9 +1,170 @@ -# Types and Subpackets +# Types and Sub Packets In this section, we will learn how to use `ElvenGard.Network.Type`. We're going to create all the types and subpackets needed to serialize and deserialize our packets according to our [network protocol](network_protocol.html). - ## Types + +So, according to our network protocol, we must define theses types: + +| Field Type | Encoded or Decoded | Used by | +|------------|--------------------|---------------------------| +| String | Both | LoginRequest, LoginFailed | +| DateTime | Encoded | PongResponse | + +Let's start with the first one: + +```elixir +# file: lib/types/string_type.ex +defmodule LoginServer.Types.StringType do + @moduledoc """ + Documentation for LoginServer.Types.StringType + """ + + use ElvenGard.Network.Type + + @type t :: String.t() + + ## Behaviour impls + + @impl true + @spec decode(binary(), Keyword.t()) :: {t(), binary()} + def decode(data, _opts) when is_binary(data) do + case String.split(data, " ", parts: 2) do + [string] -> {string, ""} + [string, rest] -> {string, rest} + end + end + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, _opts) when is_binary(data) do + data + end +end +``` + +As you can see, we just need to use `ElvenGard.Network.Type` and define 2 callbacks: + + - `decode/2`: takes the binary to be decoded and options. This callback must return a + tuple in the form `{type_decoded, rest_of_binary_not_decoded}`. + - `encode/2`: takes the type to encode and options. This callback must return an + [iodata](https://hexdocs.pm/elixir/IO.html#module-io-data) representation of our type. + +**NOTE**: Typespecs and guards are not mandatory, but are a good practice. + +```elixir +# file: lib/types/date_time_type.ex +defmodule LoginServer.Types.DateTimeType do + @moduledoc """ + Documentation for LoginServer.Types.DateTimeType + """ + + use ElvenGard.Network.Type + + @type t :: DateTime.t() + + ## Behaviour impls + + @impl true + def decode(_data, _opts), do: raise("unimplemented") + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, _opts) when is_struct(data, DateTime) do + DateTime.to_string(data) + end +end +``` + +For this second type it's the same, the only difference is that we don't need to need +to deserialize a `DateTimeType`, so we can just ignore the function and raise if someone +is trying to use it. + +## Sub Packets + +A Sub Packet is simply a type that uses other types. Unlike a packet, it has no ID, as it +will be used by other packets. + +Let's look at a simple example with the `WorldInfo`: + +| SubPacket Name | Encoded or Decoded | Used by | +|----------------|--------------------|--------------| +| WorldInfo | Encoded | LoginSucceed | + +```elixir +# file: lib/sub_packets/world_info.ex +defmodule LoginServer.SubPackets.WorldInfo do + @moduledoc """ + Documentation for LoginServer.SubPackets.WorldInfo + """ + + use ElvenGard.Network.Type + + alias __MODULE__ + alias LoginServer.Types.{IntegerType, StringType} + + @enforce_keys [:host, :port] + defstruct [:host, :port] + + @type t :: %WorldInfo{host: StringType.t(), port: IntegerType.t()} + + ## Behaviour impls + + @impl true + def decode(_data, _opts), do: raise("unimplemented") + + @impl true + @spec encode(t(), Keyword.t()) :: iolist() + def encode(data, opts) when is_struct(data, WorldInfo) do + separator = Keyword.fetch!(opts, :sep) + + [ + StringType.encode(data.host), + StringType.encode(separator), + IntegerType.encode(data.port) + ] + end +end +``` + +As you can see, this type is represented in Elixir by a structure with 2 mandatory fields: +`host` and `port`. They are each represented by the `StringType` and `IntegerType` types, +with which they will be encoded. + +**NOTE**: this Sub Packet has another special feature: the separator used by the fields can be +configured through options. We'll see how to use it in the next sections. + +As the `IntegerType` type has not been created yet, let's create it defining only the +`encode/2` function. + +```elixir +# file: lib/types/integer_type.ex +defmodule LoginServer.Types.IntegerType do + @moduledoc """ + Documentation for LoginServer.Types.IntegerType + """ + + use ElvenGard.Network.Type + + @type t :: integer() + + ## Behaviour impls + + @impl true + def decode(_data, _opts), do: raise("unimplemented") + + @impl true + @spec encode(t(), Keyword.t()) :: binary() + def encode(data, _opts) when is_integer(data) do + Integer.to_string(data) + end +end +``` + +## Summary + +In this guide, we've seen how to create customizable types and sub-packets. Now we'll look at +how to use them. From 3f9af17089d26e243b467865afe5a362d6458806 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:55:36 +0200 Subject: [PATCH 46/59] :memo: Add guide file structure --- guides/introduction/network_codec.md | 3 +++ guides/introduction/packet_definitions.md | 3 +++ guides/introduction/packet_handlers.md | 3 +++ guides/introduction/packet_views.md | 3 +++ guides/introduction/types_and_subpackets.md | 2 +- mix.exs | 8 ++++++-- 6 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 guides/introduction/network_codec.md create mode 100644 guides/introduction/packet_definitions.md create mode 100644 guides/introduction/packet_handlers.md create mode 100644 guides/introduction/packet_views.md diff --git a/guides/introduction/network_codec.md b/guides/introduction/network_codec.md new file mode 100644 index 0000000..253465b --- /dev/null +++ b/guides/introduction/network_codec.md @@ -0,0 +1,3 @@ +# Network Codec + +TODO diff --git a/guides/introduction/packet_definitions.md b/guides/introduction/packet_definitions.md new file mode 100644 index 0000000..4e54230 --- /dev/null +++ b/guides/introduction/packet_definitions.md @@ -0,0 +1,3 @@ +# Packet Definitions + +TODO diff --git a/guides/introduction/packet_handlers.md b/guides/introduction/packet_handlers.md new file mode 100644 index 0000000..1c9c15c --- /dev/null +++ b/guides/introduction/packet_handlers.md @@ -0,0 +1,3 @@ +# Packet Handlers + +TODO diff --git a/guides/introduction/packet_views.md b/guides/introduction/packet_views.md new file mode 100644 index 0000000..aae4395 --- /dev/null +++ b/guides/introduction/packet_views.md @@ -0,0 +1,3 @@ +# Packet Views + +TODO diff --git a/guides/introduction/types_and_subpackets.md b/guides/introduction/types_and_subpackets.md index 484261b..0e16c1c 100644 --- a/guides/introduction/types_and_subpackets.md +++ b/guides/introduction/types_and_subpackets.md @@ -167,4 +167,4 @@ end ## Summary In this guide, we've seen how to create customizable types and sub-packets. Now we'll look at -how to use them. +how to encode them over the network. diff --git a/mix.exs b/mix.exs index 0bd166a..9f4e892 100644 --- a/mix.exs +++ b/mix.exs @@ -77,7 +77,11 @@ defmodule ElvenGard.Network.MixProject do "guides/introduction/getting_started.md", "guides/introduction/endpoint.md", "guides/introduction/protocol.md", - "guides/introduction/types_and_subpackets.md" + "guides/introduction/types_and_subpackets.md", + "guides/introduction/network_codec.md", + "guides/introduction/packet_definitions.md", + "guides/introduction/packet_views.md", + "guides/introduction/packet_handlers.md" ] ) end @@ -94,7 +98,7 @@ defmodule ElvenGard.Network.MixProject do # ElvenGard.Network # ElvenGard.Network.Socket # ElvenGard.Network.Type - # ElvenGard.Network.View + # ElvenGard.Network.View [ Endpoint: [ From dcc1a330478893393ff12040416b242cc9a4ce2b Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:58:01 +0200 Subject: [PATCH 47/59] :memo: Typo --- guides/introduction/protocol.md | 2 +- guides/introduction/types_and_subpackets.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md index a098c8a..7fbac5d 100644 --- a/guides/introduction/protocol.md +++ b/guides/introduction/protocol.md @@ -27,7 +27,7 @@ For this part, we're going to create a fairly simple protocol that will just dis our packets. ```elixir -# file: lib/endpoint/protocol.ex +# file: lib/login_server/endpoint/protocol.ex defmodule LoginServer.Endpoint.Protocol do @moduledoc """ Documentation for LoginServer.Endpoint.Protocol diff --git a/guides/introduction/types_and_subpackets.md b/guides/introduction/types_and_subpackets.md index 0e16c1c..9bff861 100644 --- a/guides/introduction/types_and_subpackets.md +++ b/guides/introduction/types_and_subpackets.md @@ -17,7 +17,7 @@ So, according to our network protocol, we must define theses types: Let's start with the first one: ```elixir -# file: lib/types/string_type.ex +# file: lib/login_server/types/string_type.ex defmodule LoginServer.Types.StringType do @moduledoc """ Documentation for LoginServer.Types.StringType @@ -56,7 +56,7 @@ As you can see, we just need to use `ElvenGard.Network.Type` and define 2 callba **NOTE**: Typespecs and guards are not mandatory, but are a good practice. ```elixir -# file: lib/types/date_time_type.ex +# file: lib/login_server/types/date_time_type.ex defmodule LoginServer.Types.DateTimeType do @moduledoc """ Documentation for LoginServer.Types.DateTimeType @@ -95,7 +95,7 @@ Let's look at a simple example with the `WorldInfo`: | WorldInfo | Encoded | LoginSucceed | ```elixir -# file: lib/sub_packets/world_info.ex +# file: lib/login_server/sub_packets/world_info.ex defmodule LoginServer.SubPackets.WorldInfo do @moduledoc """ Documentation for LoginServer.SubPackets.WorldInfo @@ -141,7 +141,7 @@ As the `IntegerType` type has not been created yet, let's create it defining onl `encode/2` function. ```elixir -# file: lib/types/integer_type.ex +# file: lib/login_server/types/integer_type.ex defmodule LoginServer.Types.IntegerType do @moduledoc """ Documentation for LoginServer.Types.IntegerType From 1b777d0c7089d224043d21fca422fa1452aaed1c Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:56:45 +0200 Subject: [PATCH 48/59] :memo: Update branch name --- .github/workflows/elixir.yml | 4 ++-- README.md | 6 +++--- guides/introduction/getting_started.md | 2 +- guides/introduction/types_and_subpackets.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 96cf86f..27a9ed5 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -3,10 +3,10 @@ name: Elixir CI on: push: branches: - - master + - main pull_request: branches: - - master + - main permissions: contents: read diff --git a/README.md b/README.md index 733d067..1460ddd 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Build Status](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml/badge.svg?branch=master)](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml) -[![Coverage Status](https://coveralls.io/repos/github/ImNotAVirus/elvengard_network/badge.svg?branch=master)](https://coveralls.io/github/ImNotAVirus/elvengard_network?branch=master) +[![Build Status](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml/badge.svg?branch=main)](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml) +[![Coverage Status](https://coveralls.io/repos/github/ImNotAVirus/elvengard_network/badge.svg?branch=main)](https://coveralls.io/github/ImNotAVirus/elvengard_network?branch=main) ## What is ElvenGard @@ -48,7 +48,7 @@ The docs can be found at [https://hexdocs.pm/elvengard_network](https://hexdocs. ## Projects using ElvenGard -- [MinecraftEx](https://github.com/ImNotAVirus/elvengard_network/tree/master/examples/minecraft_ex): Located in the `examples` folder of the repository, this is the beginning of a [Minecraft](https://www.minecraft.net) server emulator +- [MinecraftEx](https://github.com/ImNotAVirus/elvengard_network/tree/main/examples/minecraft_ex): Located in the `examples` folder of the repository, this is the beginning of a [Minecraft](https://www.minecraft.net) server emulator - [AvantHeim](https://github.com/ImNotAVirus/AvantHeim): Created by the same developer as ElvenGard, this is a [NosTale](https://gameforge.com/en-US/play/nostale) server emulator ## Contributing diff --git a/guides/introduction/getting_started.md b/guides/introduction/getting_started.md index 79a2d29..2f40e05 100644 --- a/guides/introduction/getting_started.md +++ b/guides/introduction/getting_started.md @@ -4,7 +4,7 @@ This guide is an introduction to [ElvenGard.Network](https://github.com/ImNotAVi The purpose of this toolkit is to provide a set of modules and processes to simplify the creation of a game server. It's therefore simple to use and flexible in order to allow you to concentrate on the implementation of your features. In this guide, we're going to learn some basics about ElvenGard.Network. If you want -to see the code from this guide, you can view it at [elvengard_network/examples/login_server on GitHub](https://github.com/ImNotAVirus/elvengard_network/tree/master/examples/login_server). +to see the code from this guide, you can view it at [elvengard_network/examples/login_server on GitHub](https://github.com/ImNotAVirus/elvengard_network/tree/main/examples/login_server). ## Adding ElvenGard.Network to an application diff --git a/guides/introduction/types_and_subpackets.md b/guides/introduction/types_and_subpackets.md index 9bff861..bcc0a76 100644 --- a/guides/introduction/types_and_subpackets.md +++ b/guides/introduction/types_and_subpackets.md @@ -167,4 +167,4 @@ end ## Summary In this guide, we've seen how to create customizable types and sub-packets. Now we'll look at -how to encode them over the network. +how to use them. From f73180a35c9acf6447008a3ed5692e5f3c69cf94 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:57:44 +0200 Subject: [PATCH 49/59] :memo: WIP guide for Network Codec --- guides/introduction/network_codec.md | 47 +++++++++++++++++++++++++++- mix.exs | 2 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/guides/introduction/network_codec.md b/guides/introduction/network_codec.md index 253465b..4086c66 100644 --- a/guides/introduction/network_codec.md +++ b/guides/introduction/network_codec.md @@ -1,3 +1,48 @@ # Network Codec -TODO +```elixir +# file: lib/login_server/endpoint/network_codec.ex +defmodule LoginServer.Endpoint.NetworkCodec do + @moduledoc """ + Documentation for LoginServer.Endpoint.NetworkCodec + """ + + @behaviour ElvenGard.Network.NetworkCodec + + alias LoginServer.ClientPackets + + ## Behaviour impls + + @impl true + def next(raw, _socket) do + case String.split(raw, "\n", parts: 2) do + [packet] -> {packet, ""} + [packet, rest] -> {packet, rest} + end + end + + @impl true + def decode(raw, socket) do + case String.split(raw, " ", parts: 2) do + [packet_id] -> ClientPackets.deserialize(packet_id, "", socket) + [packet_id, params] -> ClientPackets.deserialize(packet_id, params, socket) + end + end + + @impl true + def encode(struct, _socket) when is_struct(struct) do + # %LoginSucceed{world_info: %WorldInfo{host: "127.0.0.1", port: 5000}} + struct + # {"SUCCESS", ["127.0.0.1", ":", "5000"]} + |> struct.__struct__.serialize() + # ["SUCCESS", ["127.0.0.1", ":", "5000"]] + |> Tuple.to_list() + # ["SUCCESS", " ", ["127.0.0.1", ":", "5000"]] + |> Enum.intersperse(" ") + # [["SUCCESS", " ", ["127.0.0.1", ":", "5000"]], "\n"] + |> then(&[&1 | "\n"]) + + # This will be serialized into `SUCCESS 127.0.0.1:5000\n` + end +end +``` diff --git a/mix.exs b/mix.exs index 9f4e892..d9012eb 100644 --- a/mix.exs +++ b/mix.exs @@ -78,8 +78,8 @@ defmodule ElvenGard.Network.MixProject do "guides/introduction/endpoint.md", "guides/introduction/protocol.md", "guides/introduction/types_and_subpackets.md", - "guides/introduction/network_codec.md", "guides/introduction/packet_definitions.md", + "guides/introduction/network_codec.md", "guides/introduction/packet_views.md", "guides/introduction/packet_handlers.md" ] From 5f054f74136dac8bcbd1a6cc460bca37c18e6735 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:58:33 +0200 Subject: [PATCH 50/59] :memo: WIP guide for Packet Definitions --- guides/introduction/packet_definitions.md | 142 +++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/guides/introduction/packet_definitions.md b/guides/introduction/packet_definitions.md index 4e54230..bbd99fa 100644 --- a/guides/introduction/packet_definitions.md +++ b/guides/introduction/packet_definitions.md @@ -1,3 +1,143 @@ # Packet Definitions -TODO +In this section, we will learn how to use `ElvenGard.Network.PacketSerializer`. + +We'll use the previously created types to transform our +[network protocol](network_protocol.html) into Elixir structures. + +## `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: + +```elixir +# 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`. + +```elixir +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: + +```elixir +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](https://github.com/ImNotAVirus/elvengard_network/tree/main/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. + +```elixir +defpacket 0x0000, as: MyPacket do + field ... +end +``` + +## `field` Macro + +- type +- options + + +## Decorators + + + +## serialize/1, deserialize/3 (both) + + + +## Client Packets + +```elixir +# 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 +``` + +## Server Packets + +```elixir +# 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 +``` From d1403d437303af65c5c137ef75d54bc7ad381275 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:43:15 +0200 Subject: [PATCH 51/59] :memo: Add guide for Packet Definitions --- TODOLIST.md | 1 + guides/introduction/packet_definitions.md | 65 ++++++++++++++++++--- guides/introduction/protocol.md | 2 +- guides/introduction/types_and_subpackets.md | 4 +- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/TODOLIST.md b/TODOLIST.md index 04b9118..117cbe6 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -6,3 +6,4 @@ - Document all `use` according to the bests practices - Add telemetry - PacketHandlers behaviour +- use @derive for packet serialization/deserialization instead of `@serializable` and `@deserializable` ?? diff --git a/guides/introduction/packet_definitions.md b/guides/introduction/packet_definitions.md index bbd99fa..69fcd97 100644 --- a/guides/introduction/packet_definitions.md +++ b/guides/introduction/packet_definitions.md @@ -5,7 +5,9 @@ In this section, we will learn how to use `ElvenGard.Network.PacketSerializer`. We'll use the previously created types to transform our [network protocol](network_protocol.html) into Elixir structures. -## `defpacket` Macro +## Macros + +### `defpacket` Macro First, we'll take a look at how the `ElvenGard.Network.PacketSerializer.defpacket/3` macro works. @@ -35,7 +37,7 @@ defmodule MyApp do end ``` -### Guards +#### 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 @@ -55,7 +57,7 @@ For more information and examples of how to use guards, I recommend you read the **NOTE**: guards are only available for deserialization (client packets). -### Body +#### Body Finally, to define fields in our packet, their name, type etc., we'll use a `do ... end` block. @@ -65,22 +67,52 @@ defpacket 0x0000, as: MyPacket do 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: + +```elixir +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 `c:ElvenGard.Network.Type.encode/2` +and `c:ElvenGard.Network.Type.decode/2`). -- type -- options +### 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. -## Decorators +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. -## serialize/1, deserialize/3 (both) +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](network_codec.html), +as we'll see in the next chapter. +Now that we know the theory, it's time to put it into practice. ## Client Packets +Let's first define our client packets. + ```elixir # file: lib/login_server/client_packets.ex defmodule LoginServer.ClientPackets do @@ -107,8 +139,14 @@ defmodule LoginServer.ClientPackets do 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 +Now let's define the server's one. + ```elixir # file: lib/login_server/server_packets.ex defmodule LoginServer.ServerPackets do @@ -141,3 +179,14 @@ defmodule LoginServer.ServerPackets do 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 + +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. diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md index 7fbac5d..2604bbd 100644 --- a/guides/introduction/protocol.md +++ b/guides/introduction/protocol.md @@ -21,7 +21,7 @@ Here's the explanation for these options: - `network_codec: LoginServer.NetworkCodec`: Specifies the module responsible for encoding and decoding packets for communication between clients and the server. -## Creating a Protocol +## Create a Protocol For this part, we're going to create a fairly simple protocol that will just display our packets. diff --git a/guides/introduction/types_and_subpackets.md b/guides/introduction/types_and_subpackets.md index bcc0a76..5eb6ba9 100644 --- a/guides/introduction/types_and_subpackets.md +++ b/guides/introduction/types_and_subpackets.md @@ -85,7 +85,7 @@ is trying to use it. ## Sub Packets -A Sub Packet is simply a type that uses other types. Unlike a packet, it has no ID, as it +A sub-packet is simply a type that uses other types. Unlike a packet, it has no ID, as it will be used by other packets. Let's look at a simple example with the `WorldInfo`: @@ -134,7 +134,7 @@ As you can see, this type is represented in Elixir by a structure with 2 mandato `host` and `port`. They are each represented by the `StringType` and `IntegerType` types, with which they will be encoded. -**NOTE**: this Sub Packet has another special feature: the separator used by the fields can be +**NOTE**: this sub-packet has another special feature: the separator used by the fields can be configured through options. We'll see how to use it in the next sections. As the `IntegerType` type has not been created yet, let's create it defining only the From a0ce9ba785a2e5e4cd61c44140138635a8bac086 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 16 Aug 2023 20:28:32 +0200 Subject: [PATCH 52/59] :memo: Add guide for Network Codec --- TODOLIST.md | 1 + guides/introduction/network_codec.md | 31 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/TODOLIST.md b/TODOLIST.md index 117cbe6..972b516 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -7,3 +7,4 @@ - Add telemetry - PacketHandlers behaviour - use @derive for packet serialization/deserialization instead of `@serializable` and `@deserializable` ?? +- mix task `elven_network.new` to create a project structure diff --git a/guides/introduction/network_codec.md b/guides/introduction/network_codec.md index 4086c66..eb9d648 100644 --- a/guides/introduction/network_codec.md +++ b/guides/introduction/network_codec.md @@ -1,5 +1,23 @@ # Network Codec +In this section, we will learn how to use `ElvenGard.Network.NetWorkCodec`. + +A NetworkCodec is a behaviour that define how a packet should transit over the network. +It must define 3 callbacks : + + - `next/2`: this callback takes a raw binary and a socket as parameters. It returns + the first packet found in the form of a tuple: `{packet_binary, rest_of_binary}`. + - `decode/2`: this callback takes the binary return by `next/2` and returns the + deserialized packet (a structure). You should use the `deserialize/3` helper created + by [defpacket](packet_definitions.html#decorators) + - `encode/2`: this callback will be called when you call `ElvenGard.Network.Socket.send/2` + with your module as encoder. It returns an + [iodata](https://hexdocs.pm/elixir/IO.html#module-io-data) that will, then, be send to + your client. + +## Create a Network Codec + + ```elixir # file: lib/login_server/endpoint/network_codec.ex defmodule LoginServer.Endpoint.NetworkCodec do @@ -42,7 +60,18 @@ defmodule LoginServer.Endpoint.NetworkCodec do # [["SUCCESS", " ", ["127.0.0.1", ":", "5000"]], "\n"] |> then(&[&1 | "\n"]) - # This will be serialized into `SUCCESS 127.0.0.1:5000\n` + # This iolist will be send as `SUCCESS 127.0.0.1:5000\n` end end ``` + +Here, our `decode/2` callback uses `LoginServer.ClientPackets.deserialize/3`, which +redirects to `LoginServer.ClientPackets..deserialize/3`. +Note also that `encode/2` uses `LoginServer.ServerPackets..serialize/1` +to serialize our packet. Finally, we add a `\n` at the end of our packet as defined by +the [network protocol](network_protocol.html). + +## Summary + +Now that we've defined our functions for transforming data structures, it's time to see how +to link them to our [Protocol](protocol.html). From 4672c91e381086b19d8a3930aa54ff0cffa93d53 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sun, 20 Aug 2023 20:38:01 +0200 Subject: [PATCH 53/59] :memo: Add guide for Packet Handler --- TODOLIST.md | 1 - examples/login_server/config/config.exs | 2 +- .../{packet_handlers.ex => packet_handler.ex} | 5 +- examples/minecraft_ex/config/config.exs | 2 +- .../{packet_handlers.ex => packet_handler.ex} | 4 +- guides/introduction/getting_started.md | 4 +- guides/introduction/network_codec.md | 3 +- guides/introduction/packet_handler.md | 87 +++++++++++++++++++ guides/introduction/packet_handlers.md | 3 - guides/introduction/protocol.md | 2 +- lib/elven_gard/network/packet_handler.ex | 31 +++++++ mix.exs | 3 +- ...ma_test.exs => packet_serializer_test.exs} | 0 13 files changed, 133 insertions(+), 14 deletions(-) rename examples/login_server/lib/login_server/endpoint/{packet_handlers.ex => packet_handler.ex} (90%) rename examples/minecraft_ex/lib/minecraft_ex/endpoint/{packet_handlers.ex => packet_handler.ex} (93%) create mode 100644 guides/introduction/packet_handler.md delete mode 100644 guides/introduction/packet_handlers.md create mode 100644 lib/elven_gard/network/packet_handler.ex rename test/elven_gard/network/{packet_schema_test.exs => packet_serializer_test.exs} (100%) diff --git a/TODOLIST.md b/TODOLIST.md index 972b516..6f7a525 100644 --- a/TODOLIST.md +++ b/TODOLIST.md @@ -5,6 +5,5 @@ - Refacto ElvenGard.Network.Endpoint.Protocol - Document all `use` according to the bests practices - Add telemetry -- PacketHandlers behaviour - use @derive for packet serialization/deserialization instead of `@serializable` and `@deserializable` ?? - mix task `elven_network.new` to create a project structure diff --git a/examples/login_server/config/config.exs b/examples/login_server/config/config.exs index 04a7520..1271607 100644 --- a/examples/login_server/config/config.exs +++ b/examples/login_server/config/config.exs @@ -7,5 +7,5 @@ config :login_server, LoginServer.Endpoint, protocol: LoginServer.Endpoint.Protocol config :login_server, LoginServer.Endpoint.Protocol, - packet_handler: LoginServer.Endpoint.PacketHandlers, + packet_handler: LoginServer.Endpoint.PacketHandler, network_codec: LoginServer.Endpoint.NetworkCodec diff --git a/examples/login_server/lib/login_server/endpoint/packet_handlers.ex b/examples/login_server/lib/login_server/endpoint/packet_handler.ex similarity index 90% rename from examples/login_server/lib/login_server/endpoint/packet_handlers.ex rename to examples/login_server/lib/login_server/endpoint/packet_handler.ex index f2f585c..9d8eea7 100644 --- a/examples/login_server/lib/login_server/endpoint/packet_handlers.ex +++ b/examples/login_server/lib/login_server/endpoint/packet_handler.ex @@ -1,8 +1,10 @@ -defmodule LoginServer.Endpoint.PacketHandlers do +defmodule LoginServer.Endpoint.PacketHandler do @moduledoc """ LoginServer.Endpoint.PacketHandler """ + @behaviour ElvenGard.Network.PacketHandler + alias ElvenGard.Network.Socket alias LoginServer.ClientPackets.{PingRequest, LoginRequest} @@ -10,6 +12,7 @@ defmodule LoginServer.Endpoint.PacketHandlers do ## Handlers + @impl true def handle_packet(%PingRequest{}, socket) do render = PacketViews.render(:pong_response, %{time: DateTime.utc_now()}) :ok = Socket.send(socket, render) diff --git a/examples/minecraft_ex/config/config.exs b/examples/minecraft_ex/config/config.exs index 0237bc3..c8805e7 100644 --- a/examples/minecraft_ex/config/config.exs +++ b/examples/minecraft_ex/config/config.exs @@ -13,5 +13,5 @@ config :minecraft_ex, MinecraftEx.Endpoint, protocol: MinecraftEx.Endpoint.Protocol config :minecraft_ex, MinecraftEx.Endpoint.Protocol, - packet_handler: MinecraftEx.Endpoint.PacketHandlers, + packet_handler: MinecraftEx.Endpoint.PacketHandler, network_codec: MinecraftEx.Endpoint.NetworkCodec diff --git a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handler.ex similarity index 93% rename from examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex rename to examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handler.ex index 19396a2..c856929 100644 --- a/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handlers.ex +++ b/examples/minecraft_ex/lib/minecraft_ex/endpoint/packet_handler.ex @@ -1,6 +1,6 @@ -defmodule MinecraftEx.Endpoint.PacketHandlers do +defmodule MinecraftEx.Endpoint.PacketHandler do @moduledoc """ - Documentation for MinecraftEx.Endpoint.PacketHandlers + Documentation for MinecraftEx.Endpoint.PacketHandler """ import ElvenGard.Network.Socket, only: [assign: 3] diff --git a/guides/introduction/getting_started.md b/guides/introduction/getting_started.md index 2f40e05..d87798f 100644 --- a/guides/introduction/getting_started.md +++ b/guides/introduction/getting_started.md @@ -44,11 +44,11 @@ config :login_server, LoginServer.Endpoint, protocol: LoginServer.Endpoint.Protocol config :login_server, LoginServer.Endpoint.Protocol, - packet_handler: LoginServer.Endpoint.PacketHandlers, + packet_handler: LoginServer.Endpoint.PacketHandler, network_codec: LoginServer.Endpoint.NetworkCodec ``` -This configuration example simply tells our endpoint to listen on port 3000 of the local address and defines the protocol, network encoder and packet handlers to be used. +This configuration example simply tells our endpoint to listen on port 3000 of the local address and defines the protocol, network encoder and packet handler to be used. For more details on the configuration of each module, please refer to [Endpoint](endpoint.html#configuration) and [Protocol](protocol.html#configuration) guides and [Ranch documentation](https://ninenines.eu/docs/en/ranch/2.1/guide/). diff --git a/guides/introduction/network_codec.md b/guides/introduction/network_codec.md index eb9d648..2bae944 100644 --- a/guides/introduction/network_codec.md +++ b/guides/introduction/network_codec.md @@ -13,7 +13,8 @@ It must define 3 callbacks : - `encode/2`: this callback will be called when you call `ElvenGard.Network.Socket.send/2` with your module as encoder. It returns an [iodata](https://hexdocs.pm/elixir/IO.html#module-io-data) that will, then, be send to - your client. + your client. You should use the `serialize/1` helper created by + [defpacket](packet_definitions.html#decorators) ## Create a Network Codec diff --git a/guides/introduction/packet_handler.md b/guides/introduction/packet_handler.md new file mode 100644 index 0000000..ef48d75 --- /dev/null +++ b/guides/introduction/packet_handler.md @@ -0,0 +1,87 @@ +# Packet Handler + +If you remember, in our `config.exs`, we defined the following lines: + +```elixir +config :login_server, LoginServer.Endpoint.Protocol, + packet_handler: LoginServer.Endpoint.PacketHandler, + ... +``` + +Once the packets have been decoded by our Network Codec, they will be redirected to this module. +This module must implement the `ElvenGard.Network.PacketHandler` protocol. + +A Packet Handler is the module that will manage the logic associated with our client packets. + +Let's see how to create one for our demo application. + +## Create a PacketHandler + +```elixir +# file: lib/login_server/endpoint/packet_handler.ex +defmodule LoginServer.Endpoint.PacketHandler do + @moduledoc """ + LoginServer.Endpoint.PacketHandler + """ + + @behaviour ElvenGard.Network.PacketHandler + + alias ElvenGard.Network.Socket + + alias LoginServer.ClientPackets.{PingRequest, LoginRequest} + alias LoginServer.PacketViews + + ## Handlers + + @impl true + def handle_packet(%PingRequest{}, socket) do + render = PacketViews.render(:pong_response, %{time: DateTime.utc_now()}) + :ok = Socket.send(socket, render) + {:cont, socket} + end + + def handle_packet(%LoginRequest{username: username, password: password}, socket) do + render = + if auth_using_db(username, password) do + PacketViews.render(:login_succeed, %{world: get_worlds_from_manager()}) + else + PacketViews.render(:login_failed, %{reason: "Bad credentials :/`"}) + end + + :ok = Socket.send(socket, render) + {:halt, socket} + end + + ## Fake functions + + defp auth_using_db(username, password) do + case {username, password} do + {"admin", "password"} -> true + _ -> false + end + end + + defp get_worlds_from_manager() do + %{host: "127.0.0.1", port: 5000} + end +end +``` + +Note that each handler takes as parameter a structure with decoded fields representing the packet +and the socket associated with it. A handler must return `{:cont, new_socket}` if we want to +continue receiving packets, or `{:halt, new_socket}` to close the connection to the socket +and shutdown the associated GenServer. + +Here, our handlers are quite simple: the first will just send to our client a `PongResponse` packet +with the current time and the second will return a `LoginSucceed` or a `LoginFailed` depending +on the credentials passed in parameter. + +Note also that after a `PingRequest`, we'll continue to handle packets, whereas after a +`LoginRequest` we'll automatically close the connection. + +## Summary + +In this section, we've learned how to handle client packages, create logic around them and use +the previously created [Views](packet_views.html). + +Our application is now ready for use. Let's go to the demo! diff --git a/guides/introduction/packet_handlers.md b/guides/introduction/packet_handlers.md deleted file mode 100644 index 1c9c15c..0000000 --- a/guides/introduction/packet_handlers.md +++ /dev/null @@ -1,3 +0,0 @@ -# Packet Handlers - -TODO diff --git a/guides/introduction/protocol.md b/guides/introduction/protocol.md index 2604bbd..ae8f486 100644 --- a/guides/introduction/protocol.md +++ b/guides/introduction/protocol.md @@ -10,7 +10,7 @@ Configuration is performed via the `config/config.exs` file as you did previousl ```elixir config :login_server, LoginServer.Endpoint.Protocol, - packet_handler: LoginServer.Endpoint.PacketHandlers, + packet_handler: LoginServer.Endpoint.PacketHandler, network_codec: LoginServer.Endpoint.NetworkCodec ``` diff --git a/lib/elven_gard/network/packet_handler.ex b/lib/elven_gard/network/packet_handler.ex new file mode 100644 index 0000000..f1d36ef --- /dev/null +++ b/lib/elven_gard/network/packet_handler.ex @@ -0,0 +1,31 @@ +defmodule ElvenGard.Network.PacketHandler do + @moduledoc """ + Provides a behavior for handling incoming packets. + + This module defines the callback `handle_packet/2`, which must be implemented by + modules using this behavior to process incoming packets. + + For detailed usage information, please refer to + [the packet handler guide](). + """ + + alias ElvenGard.Network.Socket + + @doc """ + This callback function is called after a packet is received and decoded by the Endpoint. + + It is responsible for processing the packet data and returning one of the following tuples: + + - `{:cont, socket}`: Indicates that packet processing is complete, and the client connection + should continue. + - `{:halt, socket}`: Indicates that packet processing is complete, and the client connection + should be terminated. + + ## Parameters + + - `packet`: The packet data as a struct. + - `socket`: The socket representing the client connection. + """ + @callback handle_packet(packet :: struct(), socket :: Socket.t()) :: + {:cont, Socket.t()} | {:halt, Socket.t()} +end diff --git a/mix.exs b/mix.exs index d9012eb..723dbdb 100644 --- a/mix.exs +++ b/mix.exs @@ -81,7 +81,7 @@ defmodule ElvenGard.Network.MixProject do "guides/introduction/packet_definitions.md", "guides/introduction/network_codec.md", "guides/introduction/packet_views.md", - "guides/introduction/packet_handlers.md" + "guides/introduction/packet_handler.md" ] ) end @@ -105,6 +105,7 @@ defmodule ElvenGard.Network.MixProject do ElvenGard.Network.Endpoint, ElvenGard.Network.Endpoint.Protocol, ElvenGard.Network.NetworkCodec, + ElvenGard.Network.PacketHandler, ElvenGard.Network.PacketSerializer ] ] diff --git a/test/elven_gard/network/packet_schema_test.exs b/test/elven_gard/network/packet_serializer_test.exs similarity index 100% rename from test/elven_gard/network/packet_schema_test.exs rename to test/elven_gard/network/packet_serializer_test.exs From 6135d49c38c8d1aceb3d783ac5533c53e204d4ce Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sun, 20 Aug 2023 21:04:13 +0200 Subject: [PATCH 54/59] :memo: Add guide for Packet Views --- guides/introduction/packet_views.md | 51 ++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/guides/introduction/packet_views.md b/guides/introduction/packet_views.md index aae4395..9073b9d 100644 --- a/guides/introduction/packet_views.md +++ b/guides/introduction/packet_views.md @@ -1,3 +1,52 @@ # Packet Views -TODO +In this section, we will learn how to use `ElvenGard.Network.View`. + +The role of a View is to prepare data to be sent via a packet structure. + +For example, we may have a packet taking a string as a field, but our backend returns a map. +Encoding our map every time we need to create a packet would be problematic, as it would +create code duplication and would be a potential source of errors. + +Other common uses are, for example, when a packet has one or more sub-packets, or when we +need to set default values for certain fields. For simplicity's sake, and to avoid having to +duplicate this logic in every function that needs to render packets, views have been created. + +## Create a Packet Views + +```elixir +# file: lib/login_server/packet_views.ex +defmodule LoginServer.PacketViews do + @moduledoc """ + Documentation for LoginServer.PacketViews + """ + + use ElvenGard.Network.View + + alias LoginServer.ServerPackets.{PongResponse, LoginFailed, LoginSucceed} + alias LoginServer.SubPackets.WorldInfo + + @impl true + def render(:pong_response, %{time: time}) do + %PongResponse{time: time} + end + + def render(:login_failed, %{reason: reason}) do + %LoginFailed{reason: reason} + end + + def render(:login_succeed, %{world: world}) do + %LoginSucceed{world: %WorldInfo{host: world.host, port: world.port}} + end +end +``` + +Not much to explain, except that the `render/2` callback takes as its first parameter an +identifier (which must be a `String` or an `Atom`) and as its second parameter attributes +enabling us to generate our packet. Most of the time, the callback returns a strucure +(a packet), but in exceptional cases it can also directly return iodata. + +## Summary + +You now know how to create Views: functions for creating server packets. In the next +chapter, we'll use this module to create and send packets to our client. From 18b85a9aa5fb1d1c0a449545a53165e26975b3f5 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sun, 20 Aug 2023 21:05:34 +0200 Subject: [PATCH 55/59] :memo: Update version --- CHANGELOG.md | 2 +- README.md | 4 ++-- guides/introduction/getting_started.md | 2 +- mix.exs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0124cd4..dcfa649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## 0.1.0-alpha +## 0.1.0 Pre-release, this is not a production ready release ! diff --git a/README.md b/README.md index 1460ddd..12dc431 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,14 @@ The package can be installed by adding `elvengard_network` to your list of depen ```elixir def deps do [ - {:elvengard_network, "~> 0.1.0-alpha"} + {:elvengard_network, "~> 0.1.0"} ] end ``` The docs can be found at [https://hexdocs.pm/elvengard_network](https://hexdocs.pm/elvengard_network). -**/!\ This application is currently not production ready !** +**/!\ This toolkit is currently not production ready !** ## Projects using ElvenGard diff --git a/guides/introduction/getting_started.md b/guides/introduction/getting_started.md index d87798f..567de1a 100644 --- a/guides/introduction/getting_started.md +++ b/guides/introduction/getting_started.md @@ -21,7 +21,7 @@ To add ElvenGard.Network to this application, just add an entry to your mix.exs: ```elixir defp deps do [ - {:elvengard_network, "~> 0.1.0-alpha"} + {:elvengard_network, "~> 0.1.0"} ] end ``` diff --git a/mix.exs b/mix.exs index 723dbdb..98ff34b 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule ElvenGard.Network.MixProject do use Mix.Project @app_name "ElvenGard.Network" - @version "0.1.0-alpha" + @version "0.1.0" @github_link "https://github.com/ImNotAVirus/elvengard_network" def project do From e9cebe74d55c57c0ac1c7b2d985b158b8db95172 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:41:13 +0200 Subject: [PATCH 56/59] :memo: Fix typo --- guides/introduction/network_codec.md | 2 +- guides/introduction/packet_handler.md | 5 +++-- guides/introduction/packet_views.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/guides/introduction/network_codec.md b/guides/introduction/network_codec.md index 2bae944..9b6c651 100644 --- a/guides/introduction/network_codec.md +++ b/guides/introduction/network_codec.md @@ -1,6 +1,6 @@ # Network Codec -In this section, we will learn how to use `ElvenGard.Network.NetWorkCodec`. +In this section, we will learn how to use `ElvenGard.Network.NetworkCodec`. A NetworkCodec is a behaviour that define how a packet should transit over the network. It must define 3 callbacks : diff --git a/guides/introduction/packet_handler.md b/guides/introduction/packet_handler.md index ef48d75..f9e380a 100644 --- a/guides/introduction/packet_handler.md +++ b/guides/introduction/packet_handler.md @@ -81,7 +81,8 @@ Note also that after a `PingRequest`, we'll continue to handle packets, whereas ## Summary -In this section, we've learned how to handle client packages, create logic around them and use +In this section, we've learned how to handle client packets, create logic around them and use the previously created [Views](packet_views.html). -Our application is now ready for use. Let's go to the demo! +Our application is now ready for use. You can view the whole source code at +[examples/login_server](https://github.com/ImNotAVirus/elvengard_network/tree/main/examples/login_server). diff --git a/guides/introduction/packet_views.md b/guides/introduction/packet_views.md index 9073b9d..4df63d6 100644 --- a/guides/introduction/packet_views.md +++ b/guides/introduction/packet_views.md @@ -48,5 +48,5 @@ enabling us to generate our packet. Most of the time, the callback returns a str ## Summary -You now know how to create Views: functions for creating server packets. In the next +You now know how to create Views: functions for creating server packets. In the final chapter, we'll use this module to create and send packets to our client. From 623c996860b1a5e8e363da9ee898078c728abcbb Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:45:06 +0200 Subject: [PATCH 57/59] :memo: Add badges to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 12dc431..165d881 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ +[![Hex.pm version](https://img.shields.io/hexpm/v/elvengard_network.svg?style=flat)](https://hex.pm/packages/elvengard_network) +[![Hex.pm license](https://img.shields.io/hexpm/l/elvengard_network.svg?style=flat)](https://hex.pm/packages/elvengard_network) [![Build Status](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml/badge.svg?branch=main)](https://github.com/ImNotAVirus/elvengard_network/actions/workflows/elixir.yml) [![Coverage Status](https://coveralls.io/repos/github/ImNotAVirus/elvengard_network/badge.svg?branch=main)](https://coveralls.io/github/ImNotAVirus/elvengard_network?branch=main) From 25c2a5d273f288b5dad847d6ad2b702609f46858 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:58:49 +0200 Subject: [PATCH 58/59] :memo: Update all guides urls --- lib/elven_gard/network/endpoint.ex | 2 +- lib/elven_gard/network/endpoint/protocol.ex | 2 +- lib/elven_gard/network/network_codec.ex | 3 ++- lib/elven_gard/network/packet_handler.ex | 4 ++-- lib/elven_gard/network/packet_serializer.ex | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/elven_gard/network/endpoint.ex b/lib/elven_gard/network/endpoint.ex index a5e0262..0422236 100644 --- a/lib/elven_gard/network/endpoint.ex +++ b/lib/elven_gard/network/endpoint.ex @@ -7,7 +7,7 @@ defmodule ElvenGard.Network.Endpoint do handling network traffic efficiently. For in-depth information on how to use and configure network endpoints, please - refer to the [Endpoint documentation](). + refer to the [Endpoint documentation](https://hexdocs.pm/elvengard_network/endpoint.html). """ @doc "Called just before starting the ranch listener" diff --git a/lib/elven_gard/network/endpoint/protocol.ex b/lib/elven_gard/network/endpoint/protocol.ex index e44b42d..ce4b60e 100644 --- a/lib/elven_gard/network/endpoint/protocol.ex +++ b/lib/elven_gard/network/endpoint/protocol.ex @@ -11,7 +11,7 @@ defmodule ElvenGard.Network.Endpoint.Protocol do For detailed information on implementing and using network protocols with ElvenGard.Network, please refer to the - [Endpoint Protocol guide](). + [Endpoint Protocol guide](https://hexdocs.pm/elvengard_network/protocol.html). """ alias ElvenGard.Network.Socket diff --git a/lib/elven_gard/network/network_codec.ex b/lib/elven_gard/network/network_codec.ex index 1ec78b6..33f6642 100644 --- a/lib/elven_gard/network/network_codec.ex +++ b/lib/elven_gard/network/network_codec.ex @@ -7,7 +7,8 @@ defmodule ElvenGard.Network.NetworkCodec do binary data for transmission over the network. For more information on how to implement this behavior and use packet encoding - and decoding, please refer to the [NetworkCodec guide](). + and decoding, please refer to the + [NetworkCodec guide](https://hexdocs.pm/elvengard_network/network_codec.html). """ alias ElvenGard.Network.Socket diff --git a/lib/elven_gard/network/packet_handler.ex b/lib/elven_gard/network/packet_handler.ex index f1d36ef..e2e1aa4 100644 --- a/lib/elven_gard/network/packet_handler.ex +++ b/lib/elven_gard/network/packet_handler.ex @@ -5,8 +5,8 @@ defmodule ElvenGard.Network.PacketHandler do This module defines the callback `handle_packet/2`, which must be implemented by modules using this behavior to process incoming packets. - For detailed usage information, please refer to - [the packet handler guide](). + For detailed usage information, please refer to the + [packet handler guide](https://hexdocs.pm/elvengard_network/packet_handler.html). """ alias ElvenGard.Network.Socket diff --git a/lib/elven_gard/network/packet_serializer.ex b/lib/elven_gard/network/packet_serializer.ex index d951d56..1b2a675 100644 --- a/lib/elven_gard/network/packet_serializer.ex +++ b/lib/elven_gard/network/packet_serializer.ex @@ -7,7 +7,7 @@ defmodule ElvenGard.Network.PacketSerializer do structured packets with specified fields and decode binary data into packet structs. To learn more about the available macros and how to define packet serializers, refer - to the [Packet Serializer DSL guide](). + to the [Packet Serializer DSL guide](https://hexdocs.pm/elvengard_network/packet_definitions.html). ## Packet Macros From e0f0e28d521a9f8f39fbbd68d2d88e187534dc40 Mon Sep 17 00:00:00 2001 From: ImNotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Mon, 21 Aug 2023 00:22:55 +0200 Subject: [PATCH 59/59] :memo: Update description --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 98ff34b..47facf1 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,7 @@ defmodule ElvenGard.Network.MixProject do version: @version, elixir: "~> 1.13", name: @app_name, - description: "MMORPG Game Server toolkit written in Elixir", + description: "Game server toolkit written in Elixir # Network", elixirc_paths: elixirc_paths(Mix.env()), package: package(), docs: docs(),