defmodule Odinsea.Net.Packet.Out do @moduledoc """ Outgoing packet writer ported from Java OutPacket. Handles little-endian encoding of MapleStory packet data. """ defstruct data: <<>> @type t :: %__MODULE__{ data: iodata() } @doc """ Creates a new outgoing packet. """ @spec new() :: t() def new do %__MODULE__{data: []} end @doc """ Creates a new outgoing packet with an opcode. """ @spec new(integer()) :: t() def new(opcode) when is_integer(opcode) do %__MODULE__{data: [<>]} end @doc """ Encodes a byte to the packet. """ @spec encode_byte(t(), integer()) :: t() def encode_byte(%__MODULE__{data: data}, value) when is_integer(value) do %__MODULE__{data: [data | <>]} end @doc """ Encodes a short (2 bytes) to the packet. """ @spec encode_short(t(), integer()) :: t() def encode_short(%__MODULE__{data: data}, value) when is_integer(value) do %__MODULE__{data: [data | <>]} end @doc """ Encodes an int (4 bytes) to the packet. """ @spec encode_int(t(), integer()) :: t() def encode_int(%__MODULE__{data: data}, value) when is_integer(value) do %__MODULE__{data: [data | <>]} end @doc """ Encodes a long (8 bytes) to the packet. """ @spec encode_long(t(), integer()) :: t() def encode_long(%__MODULE__{data: data}, value) when is_integer(value) do %__MODULE__{data: [data | <>]} end @doc """ Encodes a MapleStory ASCII string. Format: [2-byte length][ASCII bytes] """ @spec encode_string(t(), String.t()) :: t() def encode_string(%__MODULE__{data: data}, value) when is_binary(value) do length = byte_size(value) %__MODULE__{data: [data | <>]} end @doc """ Encodes a boolean (1 byte, 0 = false, 1 = true). """ @spec encode_bool(t(), boolean()) :: t() def encode_bool(%__MODULE__{data: data}, true) do %__MODULE__{data: [data | <<1::signed-integer-little-8>>]} end def encode_bool(%__MODULE__{data: data}, false) do %__MODULE__{data: [data | <<0::signed-integer-little-8>>]} end @doc """ Encodes a raw buffer to the packet. """ @spec encode_buffer(t(), binary()) :: t() def encode_buffer(%__MODULE__{data: data}, buffer) when is_binary(buffer) do %__MODULE__{data: [data | buffer]} end @doc """ Encodes a fixed-size buffer, padding with zeros if necessary. """ @spec encode_fixed_buffer(t(), binary(), non_neg_integer()) :: t() def encode_fixed_buffer(%__MODULE__{data: data}, buffer, size) when is_binary(buffer) do current_size = byte_size(buffer) padding = if current_size < size do <<0::size((size - current_size) * 8)>> else <<>> end truncated = binary_part(buffer, 0, min(current_size, size)) %__MODULE__{data: [data | truncated]} |> then(&%__MODULE__{data: [&1.data | padding]}) end @doc """ Encodes a timestamp (current time in milliseconds). """ @spec encode_timestamp(t()) :: t() def encode_timestamp(%__MODULE__{data: data}) do timestamp = System.system_time(:millisecond) %__MODULE__{data: [data | <>]} end @doc """ Encodes a file time (Windows FILETIME format). """ @spec encode_filetime(t(), integer()) :: t() def encode_filetime(%__MODULE__{data: data}, value) when is_integer(value) do %__MODULE__{data: [data | <>]} end @doc """ Encodes a file time for "zero" (infinite/no expiration). """ @spec encode_filetime_zero(t()) :: t() def encode_filetime_zero(%__MODULE__{data: data}) do %__MODULE__{data: [data | <<0x00::64>>]} end @doc """ Encodes a file time for "infinite". """ @spec encode_filetime_infinite(t()) :: t() def encode_filetime_infinite(%__MODULE__{data: data}) do %__MODULE__{data: [data | <<0xDD15F1C0::little-32, 0x11CE::little-16, 0xC0C0::little-16>>]} end @doc """ Encodes a position (2 ints: x, y). """ @spec encode_position(t(), {integer(), integer()}) :: t() def encode_position(%__MODULE__{data: data}, {x, y}) do %__MODULE__{data: [data | <>]} end @doc """ Encodes a short position (2 shorts: x, y). """ @spec encode_short_position(t(), {integer(), integer()}) :: t() def encode_short_position(%__MODULE__{data: data}, {x, y}) do %__MODULE__{data: [data | <>]} end @doc """ Returns the packet as a binary. """ @spec to_binary(t()) :: binary() def to_binary(%__MODULE__{data: data}) do IO.iodata_to_binary(data) end @doc """ Returns the packet as iodata (for efficient writing). """ @spec to_iodata(t()) :: iodata() def to_iodata(%__MODULE__{data: data}) do data end @doc """ Converts the packet to a hex string for debugging. """ @spec to_string(t()) :: String.t() def to_string(%__MODULE__{data: data}) do data |> IO.iodata_to_binary() |> Odinsea.Net.Hex.encode() end @doc """ Returns the current packet size in bytes. """ @spec length(t()) :: non_neg_integer() def length(%__MODULE__{data: data}) do IO.iodata_length(data) end end