Start repo, claude & kimi still vibing tho
This commit is contained in:
283
lib/odinsea/net/packet/in.ex
Normal file
283
lib/odinsea/net/packet/in.ex
Normal file
@@ -0,0 +1,283 @@
|
||||
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
|
||||
190
lib/odinsea/net/packet/out.ex
Normal file
190
lib/odinsea/net/packet/out.ex
Normal file
@@ -0,0 +1,190 @@
|
||||
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: [<<opcode::signed-integer-little-16>>]}
|
||||
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 | <<value::signed-integer-little-8>>]}
|
||||
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 | <<value::signed-integer-little-16>>]}
|
||||
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 | <<value::signed-integer-little-32>>]}
|
||||
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 | <<value::signed-integer-little-64>>]}
|
||||
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 | <<length::signed-integer-little-16, value::binary>>]}
|
||||
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 | <<timestamp::signed-integer-little-64>>]}
|
||||
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 | <<value::signed-integer-little-64>>]}
|
||||
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 | <<x::signed-integer-little-32, y::signed-integer-little-32>>]}
|
||||
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 | <<x::signed-integer-little-16, y::signed-integer-little-16>>]}
|
||||
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
|
||||
Reference in New Issue
Block a user