defmodule GW2.ChatCode.Item do
@moduledoc ~S"""
Represents a Guild Wars 2 item chat code.
Item chat codes are used by the game to link items in chat. An item link
always contains an item id and quantity, and may also include a skin id and
up to two upgrade ids.
Use this struct with `GW2.ChatCode.encode/1` and `GW2.ChatCode.decode/1`.
## Examples
iex> GW2.ChatCode.decode("[&AgH2twBA8F8AAA==]")
{:ok, %GW2.ChatCode.Item{id: 47094, quantity: 1, skin_id: nil, upgrade_ids: [24560]}}
iex> GW2.ChatCode.encode(%GW2.ChatCode.Item{id: 30691, skin_id: 13826, upgrade_ids: [91439, 91607]})
{:ok, "[&AgHjdwDgAjYAAC9lAQDXZQEA]"}
## Fields
* `:id` - the Guild Wars 2 item id.
* `:quantity` - the linked item quantity. It must fit in one byte.
* `:skin_id` - an optional skin id applied to the item.
* `:upgrade_ids` - optional upgrade component ids. Only the first two
upgrade ids are encoded, matching the chat code format.
"""
import GW2.ChatCode.Encoder
alias GW2.ChatCode.Flag
alias GW2.ChatCode.Header
@behaviour GW2.ChatCode.Encoder
@flags %{
skin_id: 0x80,
first_upgrade_id: 0x40,
second_upgrade_id: 0x20
}
@typedoc "An item link decoded from, or ready to encode as, a chat code."
@type t :: %__MODULE__{
id: non_neg_integer(),
quantity: non_neg_integer(),
skin_id: non_neg_integer() | nil,
upgrade_ids: [non_neg_integer()]
}
defstruct [
:id,
quantity: 1,
skin_id: nil,
upgrade_ids: []
]
## Encoding
@doc """
Encodes an item struct into the binary payload used inside a chat code.
Most applications should call `GW2.ChatCode.encode/1` instead, which also
adds the chat code wrapper and Base64-encodes the payload.
"""
@spec encode(t()) :: {:ok, binary()} | {:error, atom()}
def encode(%__MODULE__{} = item) do
with {:ok, header} <- Header.encode_type(:item),
{:ok, quantity} <- encode_quantity(item),
{:ok, id} <- encode_id(item.id),
{:ok, flags} <- encode_flags(item),
{:ok, payload} <- encode_payload(item) do
{:ok, header <> quantity <> id <> flags <> payload}
end
end
@spec encode_quantity(t()) :: {:ok, binary()} | {:error, :quantity_out_of_range}
defp encode_quantity(%__MODULE__{quantity: quantity}) when quantity in 0..255 do
{:ok, <<quantity::size(8)>>}
end
defp encode_quantity(%__MODULE__{}), do: {:error, :quantity_out_of_range}
@spec encode_flags(t()) :: {:ok, binary()}
defp encode_flags(%__MODULE__{skin_id: skin_id, upgrade_ids: upgrade_ids}) do
flag =
Flag.new()
|> Flag.maybe_set(@flags.skin_id, skin_id != nil)
|> Flag.maybe_set(@flags.first_upgrade_id, Enum.at(upgrade_ids, 0) != nil)
|> Flag.maybe_set(@flags.second_upgrade_id, Enum.at(upgrade_ids, 1) != nil)
{:ok, <<flag::size(8)>>}
end
@spec encode_payload(t()) :: {:ok, binary()} | {:error, atom()}
defp encode_payload(%__MODULE__{skin_id: skin_id, upgrade_ids: upgrade_ids}) do
[skin_id | Enum.take(upgrade_ids, 2)]
|> Enum.filter(&(&1 != nil))
|> Enum.map(&encode_id/1)
|> Enum.reduce({:ok, <<>>}, fn
{:ok, id}, {:ok, acc} ->
# The spec requires a 0 byte after each payload
{:ok, acc <> id <> <<0::size(8)>>}
_, {:error, _} = error ->
error
{:error, _} = error, _ ->
error
end)
end
## Decoding
@doc """
Decodes an item chat code payload into an item struct.
Most applications should call `GW2.ChatCode.decode/1` instead, which accepts
the full chat code string.
"""
@spec decode(binary()) :: {:ok, t()} | {:error, atom()}
def decode(<<quantity, id::little-24, flags, data::binary>>) do
%__MODULE__{id: id, quantity: quantity}
|> decode_payload(flags, data)
end
def decode(_), do: {:error, :invalid_code}
@spec decode_payload(t(), non_neg_integer(), binary()) :: {:ok, t()} | {:error, atom()}
defp decode_payload(%__MODULE__{} = item, flags, data) do
[
{@flags.skin_id, &decode_skin_id/2},
{@flags.first_upgrade_id, &parse_upgrade_id/2},
{@flags.second_upgrade_id, &parse_upgrade_id/2}
]
|> Enum.filter(&Flag.check?(flags, elem(&1, 0)))
|> Enum.reduce({:ok, item, data}, fn
{_flag, decoder}, {:ok, acc, data} ->
decoder.(acc, data)
_, {:error, _} = error ->
error
end)
|> case do
# For item decoding we ignore any rest data as it's not relevant
{:ok, acc, _} -> {:ok, acc}
error -> error
end
end
defp decode_skin_id(%__MODULE__{} = item, data) do
case decode_id(data) do
{:ok, {id, rest}} -> {:ok, %__MODULE__{item | skin_id: id}, rest}
{:error, _} = error -> error
end
end
defp parse_upgrade_id(%__MODULE__{upgrade_ids: upgrade_ids} = item, data) do
case decode_id(data) do
{:ok, {id, rest}} ->
{:ok, %__MODULE__{item | upgrade_ids: [id | upgrade_ids] |> Enum.reverse()}, rest}
{:error, _} = error ->
error
end
end
end