defmodule Odinsea.Net.Packet.In do @moduledoc """ Incoming packet reader ported from Java InPacket. Handles little-endian decoding of MapleStory packet data. """ defstruct data: <<>>, index: 0, length: 0 alias Odinsea.Constants.Server @type t :: %__MODULE__{ data: binary(), index: non_neg_integer(), length: non_neg_integer() } @doc """ Creates a new incoming packet from binary data. """ @spec new(binary()) :: t() def new(data) when is_binary(data) do %__MODULE__{ data: data, index: 0, length: byte_size(data) } end @doc """ Returns the remaining bytes in the packet. """ @spec remaining(t()) :: non_neg_integer() def remaining(%__MODULE__{length: length, index: index}), do: length - index @doc """ Returns true if the packet has been fully read. """ @spec empty?(t()) :: boolean() def empty?(%__MODULE__{length: length, index: index}), do: index >= length @doc """ Returns the current read position. """ @spec get_index(t()) :: non_neg_integer() def get_index(%__MODULE__{index: index}), do: index @doc """ Sets the read position. """ @spec set_index(t(), non_neg_integer()) :: t() def set_index(packet, index) when index >= 0 do %{packet | index: min(index, packet.length)} end @doc """ Skips the specified number of bytes. """ @spec skip(t(), non_neg_integer()) :: t() def skip(packet, count) when count >= 0 do %{packet | index: min(packet.index + count, packet.length)} end @doc """ Reads a byte and returns {value, updated_packet}. """ @spec decode_byte(t()) :: {integer(), t()} | :error def decode_byte(%__MODULE__{data: data, index: index, length: length}) do if index + 1 <= length do <<_::binary-size(index), value::signed-integer-little-8, _::binary>> = data {value, %__MODULE__{data: data, index: index + 1, length: length}} else :error end end @doc """ Reads a byte and returns only the value, raising on error. """ @spec decode_byte!(t()) :: integer() def decode_byte!(packet) do case decode_byte(packet) do {value, _} -> value :error -> raise "Packet underrun reading byte" end end @doc """ Reads a short (2 bytes) and returns {value, updated_packet}. """ @spec decode_short(t()) :: {integer(), t()} | :error def decode_short(%__MODULE__{data: data, index: index, length: length}) do if index + 2 <= length do <<_::binary-size(index), value::signed-integer-little-16, _::binary>> = data {value, %__MODULE__{data: data, index: index + 2, length: length}} else :error end end @doc """ Reads a short and returns only the value, raising on error. """ @spec decode_short!(t()) :: integer() def decode_short!(packet) do case decode_short(packet) do {value, _} -> value :error -> raise "Packet underrun reading short" end end @doc """ Reads an int (4 bytes) and returns {value, updated_packet}. """ @spec decode_int(t()) :: {integer(), t()} | :error def decode_int(%__MODULE__{data: data, index: index, length: length}) do if index + 4 <= length do <<_::binary-size(index), value::signed-integer-little-32, _::binary>> = data {value, %__MODULE__{data: data, index: index + 4, length: length}} else :error end end @doc """ Reads an int and returns only the value, raising on error. """ @spec decode_int!(t()) :: integer() def decode_int!(packet) do case decode_int(packet) do {value, _} -> value :error -> raise "Packet underrun reading int" end end @doc """ Reads a long (8 bytes) and returns {value, updated_packet}. """ @spec decode_long(t()) :: {integer(), t()} | :error def decode_long(%__MODULE__{data: data, index: index, length: length}) do if index + 8 <= length do <<_::binary-size(index), value::signed-integer-little-64, _::binary>> = data {value, %__MODULE__{data: data, index: index + 8, length: length}} else :error end end @doc """ Reads a long and returns only the value, raising on error. """ @spec decode_long!(t()) :: integer() def decode_long!(packet) do case decode_long(packet) do {value, _} -> value :error -> raise "Packet underrun reading long" end end @doc """ Reads a MapleStory ASCII string (length-prefixed). Format: [2-byte length][ASCII bytes] """ @spec decode_string(t()) :: {String.t(), t()} | :error def decode_string(packet) do case decode_short(packet) do {length, packet} when length >= 0 -> decode_buffer(packet, length) |> decode_string_result() _ -> :error end end defp decode_string_result({bytes, packet}) do {bytes, packet} end @doc """ Reads a string and returns only the value, raising on error. """ @spec decode_string!(t()) :: String.t() def decode_string!(packet) do case decode_string(packet) do {value, _} -> value :error -> raise "Packet underrun reading string" end end @doc """ Reads a specified number of bytes and returns {bytes, updated_packet}. """ @spec decode_buffer(t(), non_neg_integer()) :: {binary(), t()} | :error def decode_buffer(%__MODULE__{data: data, index: index, length: total_length}, count) when count >= 0 do if index + count <= total_length do <<_::binary-size(index), buffer::binary-size(count), _::binary>> = data {buffer, %__MODULE__{data: data, index: index + count, length: total_length}} else :error end end @doc """ Reads a buffer and returns only the bytes, raising on error. """ @spec decode_buffer!(t(), non_neg_integer()) :: binary() def decode_buffer!(packet, count) do case decode_buffer(packet, count) do {value, _} -> value :error -> raise "Packet underrun reading buffer" end end @doc """ Reads a boolean (1 byte, 0 = false, 1 = true). """ @spec decode_bool(t()) :: {boolean(), t()} | :error def decode_bool(packet) do case decode_byte(packet) do {0, packet} -> {false, packet} {_, packet} -> {true, packet} :error -> :error end end @doc """ Reads a boolean, raising on error. """ @spec decode_bool!(t()) :: boolean() def decode_bool!(packet) do case decode_bool(packet) do {value, _} -> value :error -> raise "Packet underrun reading bool" end end @doc """ Reads the remaining bytes in the packet. """ @spec read_remaining(t()) :: {binary(), t()} def read_remaining(%__MODULE__{data: data, index: index, length: length}) do remaining = length - index <<_::binary-size(index), buffer::binary-size(remaining), _::binary>> = data {buffer, %__MODULE__{data: data, index: length, length: length}} end @doc """ Converts the packet to a hex string for debugging. """ @spec to_hex_string(t()) :: String.t() def to_hex_string(%__MODULE__{data: data}), do: Odinsea.Net.Hex.encode(data) @doc """ Converts the packet to a hex string with position markers. """ @spec to_hex_string(t(), boolean()) :: String.t() def to_hex_string(%__MODULE__{data: data, index: index}, true) do hex_str = Odinsea.Net.Hex.encode(data) "[pos=#{index}] " <> hex_str end def to_hex_string(packet, false), do: to_hex_string(packet) @doc """ Returns the raw packet data. """ @spec to_buffer(t()) :: binary() def to_buffer(%__MODULE__{data: data}), do: data @doc """ Returns a slice of the packet data. """ @spec slice(t(), non_neg_integer(), non_neg_integer()) :: binary() def slice(%__MODULE__{data: data}, start, length) do binary_part(data, start, length) end @doc """ Returns the packet length. """ @spec length(t()) :: non_neg_integer() def length(%__MODULE__{length: length}), do: length end