defmodule GW2.ChatCode.WardrobeTemplate do
@moduledoc ~S"""
Represents a Guild Wars 2 wardrobe template (fashion template) chat code.
Wardrobe template chat codes (otherwise known as fashion templates)
contain the skins, dyes and visibility settings for the given fashion template.
Use this struct with `GW2.ChatCode.encode/1` and `GW2.ChatCode.decode/1`.
Each slot is represented by a `GW2.ChatCode.WardrobeTemplate.Skin` struct,
or `nil` when the slot is empty.
For slots with dye channels, `nil` is written as four Dye Remover ids (`1`).
## Examples
iex> GW2.ChatCode.encode(%GW2.ChatCode.WardrobeTemplate{})
{:ok, "[&DwAAAAABAAEAAQABAAAAAQABAAEAAQAAAAEAAQABAAEAAAABAAEAAQABAAAAAQABAAEAAQAAAAEAAQABAAEAAAABAAEAAQABAAAAAQABAAEAAQAAAAAAAAAAAAAAAAD/fw==]"}
iex> template = %GW2.ChatCode.WardrobeTemplate{
...> helmet: %GW2.ChatCode.WardrobeTemplate.Skin{id: 983}
...> }
iex> GW2.ChatCode.encode(template)
{:ok, "[&DwAAAAABAAEAAQABAAAAAQABAAEAAQAAAAEAAQABAAEAAAABAAEAAQABANcDAQABAAEAAQAAAAEAAQABAAEAAAABAAEAAQABAAAAAQABAAEAAQAAAAAAAAAAAAAAAAD/fw==]"}
"""
alias GW2.ChatCode.Flag
alias GW2.ChatCode.Header
alias GW2.ChatCode.WardrobeTemplate.Skin
@behaviour GW2.ChatCode.Encoder
@uint16_max 0xFF_FF
# Order matters here
@encodable_slots [
:aquabreather,
:backpiece,
:chest,
:boots,
:gloves,
:helmet,
:leggings,
:shoulders,
:outfit,
:aquatic_weapon_a,
:aquatic_weapon_b,
:weapon_a_mainhand,
:weapon_a_offhand,
:weapon_b_mainhand,
:weapon_b_offhand
]
@dye_slots [
:backpiece,
:chest,
:boots,
:gloves,
:helmet,
:leggings,
:shoulders,
:outfit
]
# Order matters here
@flags %{
aquabreather: 0x0001,
backpiece: 0x0002,
chest: 0x0004,
boots: 0x0008,
gloves: 0x0010,
helmet: 0x0020,
leggings: 0x0040,
shoulders: 0x0080,
outfit: 0x0100,
aquatic_weapon_a: 0x0200,
aquatic_weapon_b: 0x0400,
weapon_a_mainhand: 0x0800,
weapon_a_offhand: 0x1000,
weapon_b_mainhand: 0x2000,
weapon_b_offhand: 0x4000
}
@typedoc "A wardrobe template link decoded from, or ready to encode as, a chat code."
@type t :: %__MODULE__{
aquabreather: Skin.t() | nil,
backpiece: Skin.t() | nil,
chest: Skin.t() | nil,
boots: Skin.t() | nil,
gloves: Skin.t() | nil,
helmet: Skin.t() | nil,
leggings: Skin.t() | nil,
shoulders: Skin.t() | nil,
outfit: Skin.t() | nil,
aquatic_weapon_a: Skin.t() | nil,
aquatic_weapon_b: Skin.t() | nil,
weapon_a_mainhand: Skin.t() | nil,
weapon_a_offhand: Skin.t() | nil,
weapon_b_mainhand: Skin.t() | nil,
weapon_b_offhand: Skin.t() | nil
}
defstruct [
:aquabreather,
:backpiece,
:chest,
:boots,
:gloves,
:helmet,
:leggings,
:shoulders,
:outfit,
:aquatic_weapon_a,
:aquatic_weapon_b,
:weapon_a_mainhand,
:weapon_a_offhand,
:weapon_b_mainhand,
:weapon_b_offhand
]
## Encoding
@doc """
Encodes a wardrobe template 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__{} = wardrobe_template) do
with {:ok, header} <- Header.encode_type(:wardrobe_template),
skins <- get_skins(wardrobe_template),
{:ok, binary} <- encode_template(skins),
{:ok, visibility_flags} <- encode_visibility_flags(skins) do
{:ok, header <> binary <> visibility_flags}
end
end
@spec get_skins(t()) :: [{atom(), Skin.t() | nil}]
defp get_skins(%__MODULE__{} = wardrobe_template) do
Enum.map(@encodable_slots, fn slot -> {slot, Map.fetch!(wardrobe_template, slot)} end)
end
@spec encode_template([{atom(), Skin.t() | nil}]) :: {:ok, binary()} | {:error, atom()}
defp encode_template(skins) do
Enum.reduce_while(skins, {:ok, <<>>}, fn {slot, skin}, {:ok, acc} ->
case encode_slot(slot, skin) do
{:ok, binary} -> {:cont, {:ok, acc <> binary}}
{:error, _} = error -> {:halt, error}
end
end)
end
@spec encode_slot(atom(), Skin.t() | nil) :: {:ok, binary()} | {:error, atom()}
defp encode_slot(slot, nil), do: encode_skin(0, default_dyes(slot))
defp encode_slot(slot, %Skin{} = skin),
do: encode_skin(skin.id, skin.dyes || default_dyes(slot))
@spec default_dyes(atom()) :: nil | [non_neg_integer()]
defp default_dyes(slot) when slot in @dye_slots, do: [1, 1, 1, 1]
defp default_dyes(_slot), do: nil
@spec encode_skin(skin_id :: term(), dyes :: term()) ::
{:ok, binary()} | {:error, atom()}
defp encode_skin(skin_id, nil), do: encode_id(skin_id)
defp encode_skin(skin_id, [dye1, dye2, dye3, dye4]) do
with {:ok, skin_id} <- encode_id(skin_id),
{:ok, dye1} <- encode_id(dye1),
{:ok, dye2} <- encode_id(dye2),
{:ok, dye3} <- encode_id(dye3),
{:ok, dye4} <- encode_id(dye4) do
{:ok, skin_id <> dye1 <> dye2 <> dye3 <> dye4}
end
end
defp encode_skin(_skin_id, _dyes), do: {:error, :invalid_dyes}
@spec encode_id(term()) :: {:ok, binary()} | {:error, :invalid_id}
defp encode_id(id) when is_integer(id) and id in 0..@uint16_max do
{:ok, <<id::little-16>>}
end
defp encode_id(_id), do: {:error, :invalid_id}
@spec encode_visibility_flags(skins :: [{atom(), Skin.t() | nil}]) :: {:ok, binary()}
defp encode_visibility_flags(skins) do
Enum.reduce(skins, Flag.new(), fn {slot, skin}, flags ->
Flag.maybe_set(flags, Map.fetch!(@flags, slot), skin_visible?(skin))
end)
|> then(&{:ok, <<&1::little-16>>})
end
@spec skin_visible?(Skin.t() | nil) :: boolean()
defp skin_visible?(%Skin{} = skin), do: skin.visible?
# The game marks every slot visible in an empty wardrobe template.
defp skin_visible?(nil), do: true
## Decoding
@doc """
Decodes a wardrobe template chat code payload into a 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(<<
aquabreather_skin_id::little-16,
backpiece_skin_id::little-16,
backpiece_dyes::binary-size(8),
chest_skin_id::little-16,
chest_dyes::binary-size(8),
boots_skin_id::little-16,
boots_dyes::binary-size(8),
gloves_skin_id::little-16,
gloves_dyes::binary-size(8),
helmet_skin_id::little-16,
helmet_dyes::binary-size(8),
leggings_skin_id::little-16,
leggings_dyes::binary-size(8),
shoulders_skin_id::little-16,
shoulders_dyes::binary-size(8),
outfit_id::little-16,
outfit_dyes::binary-size(8),
aquatic_weapon_a_skin_id::little-16,
aquatic_weapon_b_skin_id::little-16,
weapon_a_mainhand_skin_id::little-16,
weapon_a_offhand_skin_id::little-16,
weapon_b_mainhand_skin_id::little-16,
weapon_b_offhand_skin_id::little-16,
visibility_flags::little-16
>>) do
{:ok,
%__MODULE__{
aquabreather:
decode_skin(
aquabreather_skin_id,
Flag.check?(visibility_flags, @flags.aquabreather)
),
backpiece:
decode_skin(
backpiece_skin_id,
Flag.check?(visibility_flags, @flags.backpiece),
backpiece_dyes
),
chest:
decode_skin(
chest_skin_id,
Flag.check?(visibility_flags, @flags.chest),
chest_dyes
),
boots:
decode_skin(
boots_skin_id,
Flag.check?(visibility_flags, @flags.boots),
boots_dyes
),
gloves:
decode_skin(
gloves_skin_id,
Flag.check?(visibility_flags, @flags.gloves),
gloves_dyes
),
helmet:
decode_skin(
helmet_skin_id,
Flag.check?(visibility_flags, @flags.helmet),
helmet_dyes
),
leggings:
decode_skin(
leggings_skin_id,
Flag.check?(visibility_flags, @flags.leggings),
leggings_dyes
),
shoulders:
decode_skin(
shoulders_skin_id,
Flag.check?(visibility_flags, @flags.shoulders),
shoulders_dyes
),
outfit:
decode_skin(
outfit_id,
Flag.check?(visibility_flags, @flags.outfit),
outfit_dyes
),
aquatic_weapon_a:
decode_skin(
aquatic_weapon_a_skin_id,
Flag.check?(visibility_flags, @flags.aquatic_weapon_a)
),
aquatic_weapon_b:
decode_skin(
aquatic_weapon_b_skin_id,
Flag.check?(visibility_flags, @flags.aquatic_weapon_b)
),
weapon_a_mainhand:
decode_skin(
weapon_a_mainhand_skin_id,
Flag.check?(visibility_flags, @flags.weapon_a_mainhand)
),
weapon_a_offhand:
decode_skin(
weapon_a_offhand_skin_id,
Flag.check?(visibility_flags, @flags.weapon_a_offhand)
),
weapon_b_mainhand:
decode_skin(
weapon_b_mainhand_skin_id,
Flag.check?(visibility_flags, @flags.weapon_b_mainhand)
),
weapon_b_offhand:
decode_skin(
weapon_b_offhand_skin_id,
Flag.check?(visibility_flags, @flags.weapon_b_offhand)
)
}}
end
def decode(_), do: {:error, :invalid_code}
@spec decode_skin(non_neg_integer(), boolean(), binary() | nil) :: Skin.t() | nil
defp decode_skin(id, visible?, dyes_binary \\ nil)
defp decode_skin(0, _visible?, _dyes_binary), do: nil
defp decode_skin(id, visible?, dyes_binary) do
%Skin{
id: id,
visible?: visible?,
dyes: decode_dyes(dyes_binary)
}
end
@spec decode_dyes(nil) :: nil
defp decode_dyes(nil), do: nil
@spec decode_dyes(binary()) :: [non_neg_integer()]
defp decode_dyes(<<dye1::little-16, dye2::little-16, dye3::little-16, dye4::little-16>>) do
[dye1, dye2, dye3, dye4]
end
end