Start repo, claude & kimi still vibing tho
This commit is contained in:
124
lib/odinsea/net/cipher/aes_cipher.ex
Normal file
124
lib/odinsea/net/cipher/aes_cipher.ex
Normal file
@@ -0,0 +1,124 @@
|
||||
defmodule Odinsea.Net.Cipher.AESCipher do
|
||||
@moduledoc """
|
||||
MapleStory AES cipher implementation (AES in ECB mode with custom IV handling).
|
||||
This is used for encrypting/decrypting packet data.
|
||||
|
||||
Ported from: src/handling/netty/cipher/AESCipher.java
|
||||
"""
|
||||
|
||||
@block_size 1460
|
||||
|
||||
# MapleStory AES key (32 bytes, expanded from the Java version)
|
||||
@aes_key <<
|
||||
0x13, 0x00, 0x00, 0x00,
|
||||
0x08, 0x00, 0x00, 0x00,
|
||||
0x06, 0x00, 0x00, 0x00,
|
||||
0xB4, 0x00, 0x00, 0x00,
|
||||
0x1B, 0x00, 0x00, 0x00,
|
||||
0x0F, 0x00, 0x00, 0x00,
|
||||
0x33, 0x00, 0x00, 0x00,
|
||||
0x52, 0x00, 0x00, 0x00
|
||||
>>
|
||||
|
||||
@doc """
|
||||
Encrypts or decrypts packet data in place using AES-ECB with IV.
|
||||
|
||||
## Parameters
|
||||
- data: Binary data to encrypt/decrypt
|
||||
- iv: 4-byte IV binary
|
||||
|
||||
## Returns
|
||||
- Encrypted/decrypted binary data
|
||||
"""
|
||||
@spec crypt(binary(), binary()) :: binary()
|
||||
def crypt(data, <<_::binary-size(4)>> = iv) when is_binary(data) do
|
||||
crypt_recursive(data, iv, 0, byte_size(data), @block_size - 4)
|
||||
end
|
||||
|
||||
# Recursive encryption/decryption function
|
||||
@spec crypt_recursive(binary(), binary(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: binary()
|
||||
defp crypt_recursive(data, _iv, start, remaining, _length) when remaining <= 0 do
|
||||
# Return the portion of data we've processed
|
||||
binary_part(data, 0, start)
|
||||
end
|
||||
|
||||
defp crypt_recursive(data, iv, start, remaining, length) do
|
||||
# Multiply the IV by 4
|
||||
seq_iv = multiply_bytes(iv, byte_size(iv), 4)
|
||||
|
||||
# Adjust length if remaining is smaller
|
||||
actual_length = min(remaining, length)
|
||||
|
||||
# Extract the portion of data to process
|
||||
data_bytes = :binary.bin_to_list(data)
|
||||
|
||||
# Process the data chunk
|
||||
{new_data_bytes, _final_seq_iv} =
|
||||
process_chunk(data_bytes, seq_iv, start, start + actual_length, 0)
|
||||
|
||||
# Convert back to binary
|
||||
new_data = :binary.list_to_bin(new_data_bytes)
|
||||
|
||||
# Continue with next chunk
|
||||
new_start = start + actual_length
|
||||
new_remaining = remaining - actual_length
|
||||
new_length = @block_size
|
||||
|
||||
crypt_recursive(new_data, iv, new_start, new_remaining, new_length)
|
||||
end
|
||||
|
||||
# Process a single chunk of data
|
||||
@spec process_chunk(list(byte()), binary(), non_neg_integer(), non_neg_integer(), non_neg_integer()) ::
|
||||
{list(byte()), binary()}
|
||||
defp process_chunk(data_bytes, seq_iv, x, end_x, _offset) when x >= end_x do
|
||||
{data_bytes, seq_iv}
|
||||
end
|
||||
|
||||
defp process_chunk(data_bytes, seq_iv, x, end_x, offset) do
|
||||
# Check if we need to re-encrypt the IV
|
||||
{new_seq_iv, new_offset} =
|
||||
if rem(offset, byte_size(seq_iv)) == 0 do
|
||||
# Encrypt the IV using AES
|
||||
encrypted_iv = aes_encrypt_block(seq_iv)
|
||||
{encrypted_iv, 0}
|
||||
else
|
||||
{seq_iv, offset}
|
||||
end
|
||||
|
||||
# XOR the data byte with the IV byte
|
||||
seq_iv_bytes = :binary.bin_to_list(new_seq_iv)
|
||||
iv_index = rem(new_offset, length(seq_iv_bytes))
|
||||
iv_byte = Enum.at(seq_iv_bytes, iv_index)
|
||||
data_byte = Enum.at(data_bytes, x)
|
||||
xor_byte = Bitwise.bxor(data_byte, iv_byte)
|
||||
|
||||
# Update the data
|
||||
updated_data = List.replace_at(data_bytes, x, xor_byte)
|
||||
|
||||
# Continue processing
|
||||
process_chunk(updated_data, new_seq_iv, x + 1, end_x, new_offset + 1)
|
||||
end
|
||||
|
||||
# Encrypt a single 16-byte block using AES-ECB
|
||||
@spec aes_encrypt_block(binary()) :: binary()
|
||||
defp aes_encrypt_block(block) do
|
||||
# Pad or truncate to 16 bytes for AES
|
||||
padded_block =
|
||||
case byte_size(block) do
|
||||
16 -> block
|
||||
size when size < 16 -> block <> :binary.copy(<<0>>, 16 - size)
|
||||
size when size > 16 -> binary_part(block, 0, 16)
|
||||
end
|
||||
|
||||
# Perform AES encryption in ECB mode
|
||||
:crypto.crypto_one_time(:aes_128_ecb, @aes_key, padded_block, true)
|
||||
end
|
||||
|
||||
# Multiply bytes - repeats the first `count` bytes of `input` `mul` times
|
||||
@spec multiply_bytes(binary(), non_neg_integer(), non_neg_integer()) :: binary()
|
||||
defp multiply_bytes(input, count, mul) do
|
||||
# Take first `count` bytes and repeat them `mul` times
|
||||
chunk = binary_part(input, 0, min(count, byte_size(input)))
|
||||
:binary.copy(chunk, mul)
|
||||
end
|
||||
end
|
||||
203
lib/odinsea/net/cipher/client_crypto.ex
Normal file
203
lib/odinsea/net/cipher/client_crypto.ex
Normal file
@@ -0,0 +1,203 @@
|
||||
defmodule Odinsea.Net.Cipher.ClientCrypto do
|
||||
@moduledoc """
|
||||
Client cryptography coordinator for MapleStory packet encryption.
|
||||
Manages send/recv IVs, packet encryption/decryption, and header encoding/decoding.
|
||||
|
||||
Ported from: src/handling/netty/ClientCrypto.java
|
||||
"""
|
||||
|
||||
use Bitwise
|
||||
|
||||
alias Odinsea.Net.Cipher.{AESCipher, IGCipher}
|
||||
|
||||
defstruct [
|
||||
:version,
|
||||
:use_custom_crypt,
|
||||
:send_iv,
|
||||
:send_iv_old,
|
||||
:recv_iv,
|
||||
:recv_iv_old
|
||||
]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
version: integer(),
|
||||
use_custom_crypt: boolean(),
|
||||
send_iv: binary(),
|
||||
send_iv_old: binary(),
|
||||
recv_iv: binary(),
|
||||
recv_iv_old: binary()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Creates a new ClientCrypto instance with random IVs.
|
||||
|
||||
## Parameters
|
||||
- version: MapleStory version number (e.g., 342)
|
||||
- use_custom_crypt: If false, uses AES encryption. If true, uses basic XOR with 0x69
|
||||
|
||||
## Returns
|
||||
- New ClientCrypto struct
|
||||
"""
|
||||
@spec new(integer(), boolean()) :: t()
|
||||
def new(version, use_custom_crypt \\ false) do
|
||||
%__MODULE__{
|
||||
version: version,
|
||||
use_custom_crypt: use_custom_crypt,
|
||||
send_iv: :crypto.strong_rand_bytes(4),
|
||||
send_iv_old: <<0, 0, 0, 0>>,
|
||||
recv_iv: :crypto.strong_rand_bytes(4),
|
||||
recv_iv_old: <<0, 0, 0, 0>>
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encrypts outgoing packet data and updates the send IV.
|
||||
|
||||
## Parameters
|
||||
- crypto: ClientCrypto state
|
||||
- data: Binary packet data to encrypt
|
||||
|
||||
## Returns
|
||||
- {updated_crypto, encrypted_data}
|
||||
"""
|
||||
@spec encrypt(t(), binary()) :: {t(), binary()}
|
||||
def encrypt(%__MODULE__{} = crypto, data) when is_binary(data) do
|
||||
# Backup current send IV
|
||||
updated_crypto = %{crypto | send_iv_old: crypto.send_iv}
|
||||
|
||||
# Encrypt the data
|
||||
encrypted_data =
|
||||
if crypto.use_custom_crypt do
|
||||
basic_cipher(data)
|
||||
else
|
||||
AESCipher.crypt(data, crypto.send_iv)
|
||||
end
|
||||
|
||||
# Update the send IV using InnoGames hash
|
||||
new_send_iv = IGCipher.inno_hash(crypto.send_iv)
|
||||
final_crypto = %{updated_crypto | send_iv: new_send_iv}
|
||||
|
||||
{final_crypto, encrypted_data}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decrypts incoming packet data and updates the recv IV.
|
||||
|
||||
## Parameters
|
||||
- crypto: ClientCrypto state
|
||||
- data: Binary packet data to decrypt
|
||||
|
||||
## Returns
|
||||
- {updated_crypto, decrypted_data}
|
||||
"""
|
||||
@spec decrypt(t(), binary()) :: {t(), binary()}
|
||||
def decrypt(%__MODULE__{} = crypto, data) when is_binary(data) do
|
||||
# Backup current recv IV
|
||||
updated_crypto = %{crypto | recv_iv_old: crypto.recv_iv}
|
||||
|
||||
# Decrypt the data
|
||||
decrypted_data =
|
||||
if crypto.use_custom_crypt do
|
||||
basic_cipher(data)
|
||||
else
|
||||
AESCipher.crypt(data, crypto.recv_iv)
|
||||
end
|
||||
|
||||
# Update the recv IV using InnoGames hash
|
||||
new_recv_iv = IGCipher.inno_hash(crypto.recv_iv)
|
||||
final_crypto = %{updated_crypto | recv_iv: new_recv_iv}
|
||||
|
||||
{final_crypto, decrypted_data}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes the packet header (4 bytes) for outgoing packets.
|
||||
Returns the raw header bytes that prefix the encrypted packet.
|
||||
|
||||
## Parameters
|
||||
- crypto: ClientCrypto state
|
||||
- data_len: Length of the packet data (excluding header)
|
||||
|
||||
## Returns
|
||||
- 4-byte binary header
|
||||
"""
|
||||
@spec encode_header_len(t(), non_neg_integer()) :: binary()
|
||||
def encode_header_len(%__MODULE__{} = crypto, data_len) do
|
||||
<<s0, s1, s2, s3>> = crypto.send_iv
|
||||
|
||||
# Calculate the encoded version
|
||||
new_version = -(crypto.version + 1) &&& 0xFFFF
|
||||
enc_version = (((new_version >>> 8) &&& 0xFF) ||| ((new_version <<< 8) &&& 0xFF00)) &&& 0xFFFF
|
||||
|
||||
# Calculate raw sequence from send IV
|
||||
raw_seq = bxor((((s3 &&& 0xFF) ||| ((s2 <<< 8) &&& 0xFF00)) &&& 0xFFFF), enc_version)
|
||||
|
||||
# Calculate raw length
|
||||
raw_len = (((data_len <<< 8) &&& 0xFF00) ||| (data_len >>> 8)) &&& 0xFFFF
|
||||
raw_len_encoded = bxor(raw_len, raw_seq)
|
||||
|
||||
# Encode as 4 bytes
|
||||
<<
|
||||
(raw_seq >>> 8) &&& 0xFF,
|
||||
raw_seq &&& 0xFF,
|
||||
(raw_len_encoded >>> 8) &&& 0xFF,
|
||||
raw_len_encoded &&& 0xFF
|
||||
>>
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes the packet header to extract the data length.
|
||||
|
||||
## Parameters
|
||||
- raw_seq: 16-bit sequence number from header
|
||||
- raw_len: 16-bit length field from header
|
||||
|
||||
## Returns
|
||||
- Decoded packet length
|
||||
"""
|
||||
@spec decode_header_len(integer(), integer()) :: integer()
|
||||
def decode_header_len(raw_seq, raw_len) do
|
||||
bxor(raw_seq, raw_len) &&& 0xFFFF
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates that the incoming packet header is correct for this connection.
|
||||
|
||||
## Parameters
|
||||
- crypto: ClientCrypto state
|
||||
- raw_seq: 16-bit sequence number from header
|
||||
|
||||
## Returns
|
||||
- true if valid, false otherwise
|
||||
"""
|
||||
@spec decode_header_valid?(t(), integer()) :: boolean()
|
||||
def decode_header_valid?(%__MODULE__{} = crypto, raw_seq) do
|
||||
<<_r0, _r1, r2, r3>> = crypto.recv_iv
|
||||
|
||||
enc_version = crypto.version &&& 0xFFFF
|
||||
seq_base = ((r2 &&& 0xFF) ||| ((r3 <<< 8) &&& 0xFF00)) &&& 0xFFFF
|
||||
|
||||
bxor((raw_seq &&& 0xFFFF), seq_base) == enc_version
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the current send IV (for handshake).
|
||||
"""
|
||||
@spec get_send_iv(t()) :: binary()
|
||||
def get_send_iv(%__MODULE__{} = crypto), do: crypto.send_iv
|
||||
|
||||
@doc """
|
||||
Gets the current recv IV (for handshake).
|
||||
"""
|
||||
@spec get_recv_iv(t()) :: binary()
|
||||
def get_recv_iv(%__MODULE__{} = crypto), do: crypto.recv_iv
|
||||
|
||||
# Basic XOR cipher with 0x69 (fallback when custom_crypt is enabled)
|
||||
@spec basic_cipher(binary()) :: binary()
|
||||
defp basic_cipher(data) do
|
||||
data
|
||||
|> :binary.bin_to_list()
|
||||
|> Enum.map(fn byte -> Bitwise.bxor(byte, 0x69) end)
|
||||
|> :binary.list_to_bin()
|
||||
end
|
||||
end
|
||||
92
lib/odinsea/net/cipher/ig_cipher.ex
Normal file
92
lib/odinsea/net/cipher/ig_cipher.ex
Normal file
@@ -0,0 +1,92 @@
|
||||
defmodule Odinsea.Net.Cipher.IGCipher do
|
||||
@moduledoc """
|
||||
InnoGames cipher implementation for MapleStory packet encryption.
|
||||
Implements the IV hashing algorithm used after each encryption operation.
|
||||
|
||||
Ported from: src/handling/netty/cipher/IGCipher.java
|
||||
"""
|
||||
|
||||
use Bitwise
|
||||
|
||||
@doc """
|
||||
Applies the InnoGames hash transformation to a 4-byte IV.
|
||||
This function mutates the IV in-place using a shuffle table and bit rotation.
|
||||
|
||||
## Parameters
|
||||
- iv: 4-byte binary IV to transform
|
||||
|
||||
## Returns
|
||||
- Transformed 4-byte binary IV
|
||||
"""
|
||||
@spec inno_hash(binary()) :: binary()
|
||||
def inno_hash(<<a, b, c, d>>) do
|
||||
# Start with the base IV for morphing
|
||||
base_iv = <<0xF2, 0x53, 0x50, 0xC6>>
|
||||
|
||||
# Apply morphKey for each byte of the original IV
|
||||
result =
|
||||
[a, b, c, d]
|
||||
|> Enum.reduce(base_iv, fn value, acc ->
|
||||
morph_key(acc, value)
|
||||
end)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Morph a 4-byte key using a single input byte value
|
||||
@spec morph_key(binary(), byte()) :: binary()
|
||||
defp morph_key(<<k0, k1, k2, k3>>, value) do
|
||||
input = value &&& 0xFF
|
||||
table = shuffle_byte(input)
|
||||
|
||||
# Apply the transformation operations
|
||||
new_k0 = k0 + shuffle_byte(k1) - input
|
||||
new_k1 = k1 - bxor(k2, table)
|
||||
new_k2 = bxor(k2, shuffle_byte(k3) + input)
|
||||
new_k3 = k3 - (k0 - table)
|
||||
|
||||
# Combine into 32-bit value (little-endian)
|
||||
val =
|
||||
(new_k0 &&& 0xFF) |||
|
||||
((new_k1 <<< 8) &&& 0xFF00) |||
|
||||
((new_k2 <<< 16) &&& 0xFF0000) |||
|
||||
((new_k3 <<< 24) &&& 0xFF000000)
|
||||
|
||||
# Rotate left by 3 bits
|
||||
rotated = ((val <<< 3) ||| (val >>> 29)) &&& 0xFFFFFFFF
|
||||
|
||||
# Extract bytes (little-endian)
|
||||
<<
|
||||
rotated &&& 0xFF,
|
||||
(rotated >>> 8) &&& 0xFF,
|
||||
(rotated >>> 16) &&& 0xFF,
|
||||
(rotated >>> 24) &&& 0xFF
|
||||
>>
|
||||
end
|
||||
|
||||
# Lookup a byte in the shuffle table
|
||||
@spec shuffle_byte(integer()) :: integer()
|
||||
defp shuffle_byte(index) do
|
||||
elem(@shuffle_table, index &&& 0xFF)
|
||||
end
|
||||
|
||||
# Shuffle table - 256 bytes used for IV transformation
|
||||
@shuffle_table {
|
||||
0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC, 0x4B, 0xE9, 0xB3, 0xE1,
|
||||
0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD, 0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0,
|
||||
0xFB, 0xA1, 0x6E, 0x66, 0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA,
|
||||
0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7, 0x2F, 0x07, 0xF3, 0xAA,
|
||||
0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9, 0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA,
|
||||
0xF9, 0x93, 0x60, 0x2D, 0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31,
|
||||
0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1, 0x04, 0x00, 0xF6, 0x5A,
|
||||
0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57, 0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5,
|
||||
0xA3, 0x70, 0xBB, 0x14, 0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62,
|
||||
0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A, 0x1C, 0x88, 0x58, 0x2C,
|
||||
0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38, 0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C,
|
||||
0xA7, 0x5B, 0xA6, 0x6F, 0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF,
|
||||
0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D, 0x28, 0x0F, 0x36, 0xE3,
|
||||
0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE, 0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A,
|
||||
0x96, 0x41, 0x74, 0xAC, 0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9,
|
||||
0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05, 0xC6, 0xE5, 0x08, 0x49
|
||||
}
|
||||
end
|
||||
231
lib/odinsea/net/cipher/login_crypto.ex
Normal file
231
lib/odinsea/net/cipher/login_crypto.ex
Normal file
@@ -0,0 +1,231 @@
|
||||
defmodule Odinsea.Net.Cipher.LoginCrypto do
|
||||
@moduledoc """
|
||||
Login cryptography utilities for MapleStory authentication.
|
||||
Provides password hashing (SHA-1, SHA-512 with salt) and RSA decryption.
|
||||
|
||||
Ported from: src/client/LoginCrypto.java
|
||||
"""
|
||||
|
||||
@extra_length 6
|
||||
|
||||
@doc """
|
||||
Generates a 13-digit Asiasoft passport string.
|
||||
Format: Letter + 11 digits + Letter
|
||||
|
||||
## Returns
|
||||
- 13-character string (e.g., "A12345678901B")
|
||||
"""
|
||||
@spec generate_13digit_asiasoft_passport() :: String.t()
|
||||
def generate_13digit_asiasoft_passport do
|
||||
alphabet = ~w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
|
||||
numbers = ~w(1 2 3 4 5 6 7 8 9)
|
||||
|
||||
# First letter
|
||||
first = Enum.random(alphabet)
|
||||
|
||||
# 11 digits
|
||||
middle =
|
||||
1..11
|
||||
|> Enum.map(fn _ -> Enum.random(numbers) end)
|
||||
|> Enum.join()
|
||||
|
||||
# Last letter
|
||||
last = Enum.random(alphabet)
|
||||
|
||||
"#{first}#{middle}#{last}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Hashes a password using SHA-1.
|
||||
|
||||
## Parameters
|
||||
- password: Plain text password
|
||||
|
||||
## Returns
|
||||
- Hex-encoded SHA-1 hash (lowercase)
|
||||
"""
|
||||
@spec hex_sha1(String.t()) :: String.t()
|
||||
def hex_sha1(password) when is_binary(password) do
|
||||
hash_with_digest(password, :sha)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Hashes a password using SHA-512 with a salt.
|
||||
|
||||
## Parameters
|
||||
- password: Plain text password
|
||||
- salt: Salt string (hex-encoded)
|
||||
|
||||
## Returns
|
||||
- Hex-encoded SHA-512 hash (lowercase)
|
||||
"""
|
||||
@spec hex_sha512(String.t(), String.t()) :: String.t()
|
||||
def hex_sha512(password, salt) when is_binary(password) and is_binary(salt) do
|
||||
hash_with_digest(password <> salt, :sha512)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a SHA-1 hash matches a password.
|
||||
|
||||
## Parameters
|
||||
- hash: Expected hash (hex string, lowercase)
|
||||
- password: Password to verify
|
||||
|
||||
## Returns
|
||||
- true if matches, false otherwise
|
||||
"""
|
||||
@spec check_sha1_hash?(String.t(), String.t()) :: boolean()
|
||||
def check_sha1_hash?(hash, password) do
|
||||
hash == hex_sha1(password)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a salted SHA-512 hash matches a password.
|
||||
|
||||
## Parameters
|
||||
- hash: Expected hash (hex string, lowercase)
|
||||
- password: Password to verify
|
||||
- salt: Salt used for hashing
|
||||
|
||||
## Returns
|
||||
- true if matches, false otherwise
|
||||
"""
|
||||
@spec check_salted_sha512_hash?(String.t(), String.t(), String.t()) :: boolean()
|
||||
def check_salted_sha512_hash?(hash, password, salt) do
|
||||
hash == hex_sha512(password, salt)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies a salted SHA-512 hash against a password.
|
||||
Returns {:ok, password} on match, {:error, :invalid} on mismatch.
|
||||
|
||||
## Parameters
|
||||
- password: Password to verify
|
||||
- salt: Salt used for hashing
|
||||
- hash: Expected hash (hex string, lowercase)
|
||||
|
||||
## Returns
|
||||
- {:ok, password} if matches
|
||||
- {:error, :invalid} if doesn't match
|
||||
"""
|
||||
@spec verify_salted_sha512(String.t(), String.t(), String.t()) :: {:ok, String.t()} | {:error, :invalid}
|
||||
def verify_salted_sha512(password, salt, hash) do
|
||||
if check_salted_sha512_hash?(hash, password, salt) do
|
||||
{:ok, password}
|
||||
else
|
||||
{:error, :invalid}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a random 16-byte salt.
|
||||
|
||||
## Returns
|
||||
- Hex-encoded salt string (32 characters, lowercase)
|
||||
"""
|
||||
@spec make_salt() :: String.t()
|
||||
def make_salt do
|
||||
:crypto.strong_rand_bytes(16)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds random prefix to a string (used for RSA password encoding).
|
||||
|
||||
## Parameters
|
||||
- input: String to prefix
|
||||
|
||||
## Returns
|
||||
- String with 6 random alphanumeric characters prepended
|
||||
"""
|
||||
@spec rand_s(String.t()) :: String.t()
|
||||
def rand_s(input) when is_binary(input) do
|
||||
alphabet = ~w(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z)
|
||||
numbers = ~w(1 2 3 4 5 6 7 8 9)
|
||||
chars = alphabet ++ numbers
|
||||
|
||||
prefix =
|
||||
1..@extra_length
|
||||
|> Enum.map(fn _ -> Enum.random(chars) end)
|
||||
|> Enum.join()
|
||||
|
||||
prefix <> input
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes random prefix from a string (used for RSA password decoding).
|
||||
Extracts 128 characters starting from position 6.
|
||||
|
||||
## Parameters
|
||||
- input: String with random prefix
|
||||
|
||||
## Returns
|
||||
- Substring from position 6 to 134 (128 characters)
|
||||
"""
|
||||
@spec rand_r(String.t()) :: String.t()
|
||||
def rand_r(input) when is_binary(input) do
|
||||
String.slice(input, @extra_length, 128)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decrypts an RSA-encrypted password using the hardcoded private key.
|
||||
Uses RSA/NONE/OAEPPadding with the server's private key.
|
||||
|
||||
## Parameters
|
||||
- encrypted_password: Hex-encoded encrypted password
|
||||
|
||||
## Returns
|
||||
- Decrypted password string, or empty string on error
|
||||
"""
|
||||
@spec decrypt_rsa(String.t()) :: String.t()
|
||||
def decrypt_rsa(encrypted_password) when is_binary(encrypted_password) do
|
||||
try do
|
||||
# RSA private key parameters (from the Java version)
|
||||
modulus =
|
||||
107_657_795_738_756_861_764_863_218_740_655_861_479_186_575_385_923_787_150_128_619_142_132_921_674_398_952_720_882_614_694_082_036_467_689_482_295_621_654_506_166_910_217_557_126_105_160_228_025_353_603_544_726_428_541_751_588_805_629_215_516_978_192_030_682_053_419_499_436_785_335_057_001_573_080_195_806_844_351_954_026_120_773_768_050_428_451_512_387_703_488_216_884_037_312_069_441_551_935_633_523_181_351
|
||||
|
||||
private_exponent =
|
||||
5_550_691_850_424_331_841_608_142_211_646_492_148_529_402_295_329_912_519_344_562_675_759_756_203_942_720_314_385_192_411_176_941_288_498_447_604_817_211_202_470_939_921_344_057_999_440_566_557_786_743_767_752_684_118_754_789_131_428_284_047_255_370_747_277_972_770_485_804_010_629_706_937_510_833_543_525_825_792_410_474_569_027_516_467_052_693_380_162_536_113_699_974_433_283_374_142_492_196_735_301_185_337
|
||||
|
||||
# Decode hex string to binary
|
||||
encrypted_bytes = Base.decode16!(encrypted_password, case: :mixed)
|
||||
|
||||
# Create RSA private key
|
||||
rsa_key = [modulus, private_exponent]
|
||||
|
||||
# Decrypt using RSA with OAEP padding
|
||||
# Note: Elixir's :public_key module uses a different format
|
||||
# We need to construct the key properly
|
||||
private_key = {
|
||||
:RSAPrivateKey,
|
||||
:"two-prime",
|
||||
modulus,
|
||||
65537,
|
||||
private_exponent,
|
||||
# These are placeholders - full key derivation needed for production
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
:asn1_NOVALUE
|
||||
}
|
||||
|
||||
decrypted = :public_key.decrypt_private(encrypted_bytes, private_key, rsa_pad: :rsa_pkcs1_oaep_padding)
|
||||
|
||||
to_string(decrypted)
|
||||
rescue
|
||||
error ->
|
||||
require Logger
|
||||
Logger.error("RSA decryption failed: #{inspect(error)}")
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
# Private helper: hash a string with a given digest algorithm
|
||||
@spec hash_with_digest(String.t(), atom()) :: String.t()
|
||||
defp hash_with_digest(input, digest) do
|
||||
:crypto.hash(digest, input)
|
||||
|> Base.encode16(case: :lower)
|
||||
end
|
||||
end
|
||||
171
lib/odinsea/net/hex.ex
Normal file
171
lib/odinsea/net/hex.ex
Normal file
@@ -0,0 +1,171 @@
|
||||
defmodule Odinsea.Net.Hex do
|
||||
@moduledoc """
|
||||
Hex encoding/decoding utilities ported from Java HexTool.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Converts a binary to a hex string (space-separated bytes).
|
||||
|
||||
## Examples
|
||||
iex> Odinsea.Net.Hex.encode(<<0x01, 0xAB, 0xFF>>)
|
||||
"01 AB FF"
|
||||
"""
|
||||
@spec encode(binary()) :: String.t()
|
||||
def encode(<<>>), do: ""
|
||||
|
||||
def encode(binary) when is_binary(binary) do
|
||||
binary
|
||||
|> :binary.bin_to_list()
|
||||
|> Enum.map(&byte_to_hex/1)
|
||||
|> Enum.join(" ")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a binary to a hex string (no spaces).
|
||||
|
||||
## Examples
|
||||
iex> Odinsea.Net.Hex.to_string_compact(<<0x01, 0xAB, 0xFF>>)
|
||||
"01ABFF"
|
||||
"""
|
||||
@spec to_string_compact(binary()) :: String.t()
|
||||
def to_string_compact(<<>>), do: ""
|
||||
|
||||
def to_string_compact(binary) when is_binary(binary) do
|
||||
binary
|
||||
|> :binary.bin_to_list()
|
||||
|> Enum.map(&byte_to_hex/1)
|
||||
|> Enum.join()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a binary to a hex string with ASCII representation.
|
||||
|
||||
## Examples
|
||||
iex> Odinsea.Net.Hex.to_pretty_string(<<0x48, 0x65, 0x6C, 0x6C, 0x6F>>)
|
||||
"48 65 6C 6C 6F | Hello"
|
||||
"""
|
||||
@spec to_pretty_string(binary()) :: String.t()
|
||||
def to_pretty_string(binary) when is_binary(binary) do
|
||||
hex_part = encode(binary)
|
||||
ascii_part = to_ascii(binary)
|
||||
"#{hex_part} | #{ascii_part}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a hex string to binary.
|
||||
|
||||
## Examples
|
||||
iex> Odinsea.Net.Hex.from_string("01 AB FF")
|
||||
<<0x01, 0xAB, 0xFF>>
|
||||
"""
|
||||
@spec from_string(String.t()) :: binary()
|
||||
def from_string(hex_string) when is_binary(hex_string) do
|
||||
hex_string
|
||||
|> String.replace(~r/\s+/, "")
|
||||
|> String.downcase()
|
||||
|> do_from_string()
|
||||
end
|
||||
|
||||
defp do_from_string(<<>>), do: <<>>
|
||||
|
||||
defp do_from_string(<<hex1, hex2, rest::binary>>) do
|
||||
byte = hex_to_byte(<<hex1, hex2>>)
|
||||
<<byte, do_from_string(rest)::binary>>
|
||||
end
|
||||
|
||||
defp do_from_string(<<_>>), do: raise(ArgumentError, "Invalid hex string length")
|
||||
|
||||
@doc """
|
||||
Converts a byte to its 2-character hex representation.
|
||||
"""
|
||||
@spec byte_to_hex(byte()) :: String.t()
|
||||
def byte_to_hex(byte) when is_integer(byte) and byte >= 0 and byte <= 255 do
|
||||
byte
|
||||
|> Integer.to_string(16)
|
||||
|> String.pad_leading(2, "0")
|
||||
|> String.upcase()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a 2-character hex string to a byte.
|
||||
"""
|
||||
@spec hex_to_byte(String.t()) :: byte()
|
||||
def hex_to_byte(<<hex1, hex2>>) do
|
||||
parse_hex_digit(hex1) * 16 + parse_hex_digit(hex2)
|
||||
end
|
||||
|
||||
defp parse_hex_digit(?0), do: 0
|
||||
defp parse_hex_digit(?1), do: 1
|
||||
defp parse_hex_digit(?2), do: 2
|
||||
defp parse_hex_digit(?3), do: 3
|
||||
defp parse_hex_digit(?4), do: 4
|
||||
defp parse_hex_digit(?5), do: 5
|
||||
defp parse_hex_digit(?6), do: 6
|
||||
defp parse_hex_digit(?7), do: 7
|
||||
defp parse_hex_digit(?8), do: 8
|
||||
defp parse_hex_digit(?9), do: 9
|
||||
defp parse_hex_digit(?a), do: 10
|
||||
defp parse_hex_digit(?b), do: 11
|
||||
defp parse_hex_digit(?c), do: 12
|
||||
defp parse_hex_digit(?d), do: 13
|
||||
defp parse_hex_digit(?e), do: 14
|
||||
defp parse_hex_digit(?f), do: 15
|
||||
defp parse_hex_digit(_), do: raise(ArgumentError, "Invalid hex digit")
|
||||
|
||||
@doc """
|
||||
Converts a binary to its ASCII representation (non-printable chars as dots).
|
||||
"""
|
||||
@spec to_ascii(binary()) :: String.t()
|
||||
def to_ascii(<<>>), do: ""
|
||||
|
||||
def to_ascii(<<byte, rest::binary>>) do
|
||||
char = if byte >= 32 and byte <= 126, do: <<byte>>, else: <<".">>
|
||||
char <> to_ascii(rest)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a short (2 bytes) to a hex string.
|
||||
"""
|
||||
@spec short_to_hex(integer()) :: String.t()
|
||||
def short_to_hex(value) when is_integer(value) do
|
||||
<<value::signed-integer-little-16>>
|
||||
|> encode()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an int (4 bytes) to a hex string.
|
||||
"""
|
||||
@spec int_to_hex(integer()) :: String.t()
|
||||
def int_to_hex(value) when is_integer(value) do
|
||||
<<value::signed-integer-little-32>>
|
||||
|> encode()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats a binary as a hex dump with offsets.
|
||||
"""
|
||||
@spec hex_dump(binary(), non_neg_integer()) :: String.t()
|
||||
def hex_dump(binary, offset \\ 0) do
|
||||
binary
|
||||
|> :binary.bin_to_list()
|
||||
|> Enum.chunk_every(16)
|
||||
|> Enum.with_index()
|
||||
|> Enum.map(fn {chunk, idx} ->
|
||||
offset_str = Integer.to_string(offset + idx * 16, 16) |> String.pad_leading(8, "0")
|
||||
hex_str = format_chunk(chunk)
|
||||
ascii_str = to_ascii(:binary.list_to_bin(chunk))
|
||||
"#{offset_str} #{hex_str} |#{ascii_str}|"
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
end
|
||||
|
||||
defp format_chunk(chunk) do
|
||||
chunk
|
||||
|> Enum.map(&byte_to_hex/1)
|
||||
|> Enum.map(&String.pad_trailing(&1, 2))
|
||||
|> Enum.chunk_every(8)
|
||||
|> Enum.map(&Enum.join(&1, " "))
|
||||
|> Enum.join(" ")
|
||||
|> String.pad_trailing(48)
|
||||
end
|
||||
end
|
||||
524
lib/odinsea/net/opcodes.ex
Normal file
524
lib/odinsea/net/opcodes.ex
Normal file
@@ -0,0 +1,524 @@
|
||||
defmodule Odinsea.Net.Opcodes do
|
||||
@moduledoc """
|
||||
Packet opcodes for MapleStory GMS v342.
|
||||
Ported from Java ClientPacket and LoopbackPacket.
|
||||
"""
|
||||
|
||||
# Define the macro for creating opcodes
|
||||
defmacro defopcode(name, value) do
|
||||
quote do
|
||||
@doc """
|
||||
Opcode for `#{unquote(name)}`.
|
||||
"""
|
||||
def unquote(name)(), do: unquote(value)
|
||||
end
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Client → Server (Recv Opcodes)
|
||||
# ==================================================================================================
|
||||
|
||||
# Login/Account
|
||||
def cp_client_hello(), do: 0x01
|
||||
def cp_check_password(), do: 0x02
|
||||
def cp_world_info_request(), do: 0x04
|
||||
def cp_select_world(), do: 0x05
|
||||
def cp_check_user_limit(), do: 0x06
|
||||
def cp_check_duplicated_id(), do: 0x0E
|
||||
def cp_create_new_character(), do: 0x12
|
||||
def cp_create_ultimate(), do: 0x14
|
||||
def cp_delete_character(), do: 0x15
|
||||
def cp_select_character(), do: 0x19
|
||||
def cp_check_spw_request(), do: 0x1A
|
||||
def cp_rsa_key(), do: 0x20
|
||||
|
||||
# Connection/Security
|
||||
def cp_alive_ack(), do: 0x16
|
||||
def cp_exception_log(), do: 0x17
|
||||
def cp_security_packet(), do: 0x18
|
||||
def cp_client_dump_log(), do: 0x1D
|
||||
def cp_create_security_handle(), do: 0x1E
|
||||
def cp_hardware_info(), do: 0x21
|
||||
def cp_set_code_page(), do: 0x22
|
||||
def cp_window_focus(), do: 0x23
|
||||
def cp_inject_packet(), do: 0x24
|
||||
|
||||
# Migration/Channel
|
||||
def cp_migrate_in(), do: 0x0D
|
||||
def cp_change_channel(), do: 0x25
|
||||
def cp_enter_cash_shop(), do: 0x26
|
||||
def cp_enter_mts(), do: 0x27
|
||||
|
||||
# PVP
|
||||
def cp_enter_pvp(), do: 0x28
|
||||
def cp_enter_pvp_party(), do: 0x29
|
||||
def cp_leave_pvp(), do: 0x2A
|
||||
def cp_pvp_attack(), do: 0x2B
|
||||
def cp_pvp_respawn(), do: 0x2C
|
||||
|
||||
# Player
|
||||
def cp_move_player(), do: 0x2D
|
||||
def cp_cancel_chair(), do: 0x2F
|
||||
def cp_use_chair(), do: 0x30
|
||||
def cp_close_range_attack(), do: 0x32
|
||||
def cp_ranged_attack(), do: 0x33
|
||||
def cp_magic_attack(), do: 0x34
|
||||
def cp_passive_energy(), do: 0x35
|
||||
def cp_take_damage(), do: 0x37
|
||||
def cp_general_chat(), do: 0x39
|
||||
def cp_close_chalkboard(), do: 0x3A
|
||||
def cp_face_expression(), do: 0x3B
|
||||
def cp_face_android(), do: 0x3C
|
||||
def cp_use_item_effect(), do: 0x3D
|
||||
def cp_wheel_of_fortune(), do: 0x3E
|
||||
def cp_char_info_request(), do: 0x78
|
||||
def cp_change_keymap(), do: 0x87
|
||||
def cp_skill_macro(), do: 0x89
|
||||
def cp_change_quickslot(), do: 0x8A
|
||||
|
||||
# Movement
|
||||
def cp_change_map(), do: 0x31
|
||||
def cp_change_map_special(), do: 0x38
|
||||
def cp_use_inner_portal(), do: 0x3A
|
||||
def cp_trock_add_map(), do: 0x3B
|
||||
def cp_use_tele_rock(), do: 0x6A
|
||||
|
||||
# Combat
|
||||
def cp_distribute_ap(), do: 0x68
|
||||
def cp_auto_assign_ap(), do: 0x69
|
||||
def cp_distribute_sp(), do: 0x6C
|
||||
def cp_special_move(), do: 0x6D
|
||||
def cp_cancel_buff(), do: 0x6E
|
||||
def cp_skill_effect(), do: 0x6F
|
||||
def cp_heal_over_time(), do: 0x6A
|
||||
def cp_cancel_debuff(), do: 0x70
|
||||
def cp_give_fame(), do: 0x74
|
||||
|
||||
# Items/Inventory
|
||||
def cp_item_sort(), do: 0x4F
|
||||
def cp_item_gather(), do: 0x50
|
||||
def cp_item_move(), do: 0x51
|
||||
def cp_move_bag(), do: 0x52
|
||||
def cp_switch_bag(), do: 0x53
|
||||
def cp_use_item(), do: 0x55
|
||||
def cp_cancel_item_effect(), do: 0x56
|
||||
def cp_use_summon_bag(), do: 0x58
|
||||
def cp_pet_food(), do: 0x59
|
||||
def cp_use_mount_food(), do: 0x5A
|
||||
def cp_use_scripted_npc_item(), do: 0x5B
|
||||
def cp_use_recipe(), do: 0x5C
|
||||
def cp_use_cash_item(), do: 0x5D
|
||||
def cp_use_catch_item(), do: 0x5F
|
||||
def cp_use_skill_book(), do: 0x60
|
||||
def cp_use_owl_minerva(), do: 0x62
|
||||
def cp_use_return_scroll(), do: 0x64
|
||||
def cp_use_upgrade_scroll(), do: 0x65
|
||||
def cp_use_flag_scroll(), do: 0x66
|
||||
def cp_use_equip_scroll(), do: 0x67
|
||||
def cp_use_potential_scroll(), do: 0x68
|
||||
def cp_use_bag(), do: 0x6A
|
||||
def cp_use_magnify_glass(), do: 0x6B
|
||||
def cp_reward_item(), do: 0x8B
|
||||
def cp_item_maker(), do: 0x8C
|
||||
def cp_repair(), do: 0x8E
|
||||
def cp_repair_all(), do: 0x8D
|
||||
def cp_meso_drop(), do: 0x73
|
||||
def cp_item_pickup(), do: 0x54
|
||||
|
||||
# NPC (NOTE: These should match recvops.properties from Java server)
|
||||
def cp_npc_talk(), do: 0x40
|
||||
def cp_npc_talk_more(), do: 0x42
|
||||
def cp_npc_shop(), do: 0x43
|
||||
def cp_storage(), do: 0x44
|
||||
def cp_use_hired_merchant(), do: 0x45
|
||||
def cp_merch_item_store(), do: 0x47
|
||||
def cp_duey_action(), do: 0x48
|
||||
def cp_quest_action(), do: 0x81
|
||||
def cp_npc_move(), do: 0x41
|
||||
def cp_use_scripted_npc_item(), do: 0x59
|
||||
def cp_repair(), do: 0x89
|
||||
def cp_repair_all(), do: 0x88
|
||||
def cp_update_quest(), do: 0x142
|
||||
def cp_use_item_quest(), do: 0x144
|
||||
def cp_public_npc(), do: 0x9E
|
||||
|
||||
# Shop/Merchant
|
||||
def cp_buy_cs_item(), do: 0xB6
|
||||
def cp_coupon_code(), do: 0xB7
|
||||
def cp_cs_update(), do: 0xB8
|
||||
|
||||
# MTS
|
||||
def cp_touching_mts(), do: 0xB9
|
||||
def cp_mts_tab(), do: 0xBA
|
||||
|
||||
# Social
|
||||
def cp_party_operation(), do: 0x91
|
||||
def cp_deny_party_request(), do: 0x92
|
||||
def cp_allow_party_invite(), do: 0x93
|
||||
def cp_expedition_operation(), do: 0x94
|
||||
def cp_party_search_start(), do: 0x95
|
||||
def cp_party_search_stop(), do: 0x96
|
||||
def cp_guild_operation(), do: 0x97
|
||||
def cp_deny_guild_request(), do: 0x98
|
||||
def cp_alliance_operation(), do: 0x99
|
||||
def cp_deny_alliance_request(), do: 0x9A
|
||||
def cp_buddylist_modify(), do: 0x9B
|
||||
def cp_messenger(), do: 0x9C
|
||||
def cp_whisper(), do: 0x9D
|
||||
def cp_party_chat(), do: 0x3F
|
||||
def cp_family_operation(), do: 0x9F
|
||||
def cp_delete_junior(), do: 0xA0
|
||||
def cp_delete_senior(), do: 0xA1
|
||||
def cp_use_family(), do: 0xA2
|
||||
def cp_family_precept(), do: 0xA3
|
||||
def cp_family_summon(), do: 0xA4
|
||||
def cp_accept_family(), do: 0xA5
|
||||
def cp_request_family(), do: 0x9E
|
||||
|
||||
# Monster
|
||||
def cp_mob_move(), do: 0xA6
|
||||
def cp_mob_apply_ctrl(), do: 0xA7
|
||||
def cp_mob_hit_by_mob(), do: 0xA9
|
||||
def cp_mob_self_destruct(), do: 0xAA
|
||||
def cp_mob_time_bomb_end(), do: 0xAB
|
||||
def cp_mob_area_attack_disease(), do: 0xAC
|
||||
def cp_mob_attack_mob(), do: 0xAD
|
||||
def cp_mob_escort_collision(), do: 0xAE
|
||||
def cp_mob_request_escort_info(), do: 0xAF
|
||||
def cp_mob_skill_delay_end(), do: 0xA8
|
||||
|
||||
# NPC/Mob Interaction
|
||||
def cp_damage_reactor(), do: 0xC1
|
||||
def cp_touch_reactor(), do: 0xC2
|
||||
def cp_click_reactor(), do: 0xC2
|
||||
def cp_public_npc(), do: 0x9A
|
||||
|
||||
# Summons
|
||||
def cp_summon_attack(), do: 0xC6
|
||||
def cp_move_summon(), do: 0xC4
|
||||
def cp_damage_summon(), do: 0xC7
|
||||
def cp_sub_summon(), do: 0xC8
|
||||
def cp_remove_summon(), do: 0xC9
|
||||
def cp_move_dragon(), do: 0xCA
|
||||
|
||||
# Pets
|
||||
def cp_spawn_pet(), do: 0x79
|
||||
def cp_pet_move(), do: 0xCB
|
||||
def cp_pet_chat(), do: 0xCC
|
||||
def cp_pet_command(), do: 0xCD
|
||||
def cp_pet_drop_pickup_request(), do: 0xCE
|
||||
def cp_pet_auto_pot(), do: 0xCF
|
||||
|
||||
# Android
|
||||
def cp_move_android(), do: 0x40
|
||||
|
||||
# Familiar
|
||||
def cp_use_familiar(), do: 0x61
|
||||
def cp_spawn_familiar(), do: 0xD0
|
||||
def cp_rename_familiar(), do: 0xD1
|
||||
def cp_move_familiar(), do: 0xD2
|
||||
def cp_attack_familiar(), do: 0xD3
|
||||
def cp_touch_familiar(), do: 0xD4
|
||||
|
||||
# Events/Games
|
||||
def cp_monster_carnival(), do: 0xD5
|
||||
def cp_player_interaction(), do: 0xC0
|
||||
def cp_snowball(), do: 0xDA
|
||||
def cp_coconut(), do: 0xDB
|
||||
def cp_left_knock_back(), do: 0xD8
|
||||
|
||||
# Anti-Cheat
|
||||
def cp_user_anti_macro_item_use_request(), do: 0x7D
|
||||
def cp_user_anti_macro_skill_use_request(), do: 0x7E
|
||||
def cp_user_anti_macro_question_result(), do: 0x7F
|
||||
|
||||
# Misc
|
||||
def cp_use_door(), do: 0xBC
|
||||
def cp_use_mech_door(), do: 0xBD
|
||||
def cp_aran_combo(), do: 0x80
|
||||
def cp_transform_player(), do: 0xBA
|
||||
def cp_note_action(), do: 0xBB
|
||||
def cp_update_quest(), do: 0x85
|
||||
def cp_use_item_quest(), do: 0x86
|
||||
def cp_follow_request(), do: 0xBF
|
||||
def cp_follow_reply(), do: 0xC0
|
||||
def cp_ring_action(), do: 0xC1
|
||||
def cp_solomon(), do: 0x93
|
||||
def cp_gach_exp(), do: 0x94
|
||||
def cp_report(), do: 0xE1
|
||||
def cp_game_poll(), do: 0xE2
|
||||
def cp_ship_object(), do: 0xBE
|
||||
def cp_cygnus_summon(), do: 0xBD
|
||||
def cp_reissue_medal(), do: 0x76
|
||||
def cp_pam_song(), do: 0xE0
|
||||
|
||||
# Owl
|
||||
def cp_owl(), do: 0x4B
|
||||
def cp_owl_warp(), do: 0x4C
|
||||
|
||||
# Profession
|
||||
def cp_profession_info(), do: 0x71
|
||||
def cp_craft_done(), do: 0x72
|
||||
def cp_craft_make(), do: 0x73
|
||||
def cp_craft_effect(), do: 0x74
|
||||
def cp_start_harvest(), do: 0x75
|
||||
def cp_stop_harvest(), do: 0x76
|
||||
def cp_make_extractor(), do: 0x77
|
||||
def cp_use_bag(), do: 0x6A
|
||||
|
||||
# Pot System
|
||||
def cp_use_pot(), do: 0xE3
|
||||
def cp_clear_pot(), do: 0xE4
|
||||
def cp_feed_pot(), do: 0xE5
|
||||
def cp_cure_pot(), do: 0xE6
|
||||
def cp_reward_pot(), do: 0xE7
|
||||
|
||||
# ==================================================================================================
|
||||
# Server → Client (Send Opcodes)
|
||||
# ==================================================================================================
|
||||
|
||||
def lp_check_password_result(), do: 0x00
|
||||
def lp_guest_id_login_result(), do: 0x01
|
||||
def lp_account_info_result(), do: 0x02
|
||||
def lp_check_user_limit_result(), do: 0x03
|
||||
def lp_set_account_result(), do: 0x04
|
||||
def lp_confirm_eula_result(), do: 0x05
|
||||
def lp_check_pin_code_result(), do: 0x06
|
||||
def lp_update_pin_code_result(), do: 0x07
|
||||
def lp_view_all_char_result(), do: 0x08
|
||||
def lp_select_character_by_vac_result(), do: 0x09
|
||||
def lp_world_information(), do: 0x0A
|
||||
def lp_select_world_result(), do: 0x0B
|
||||
def lp_select_character_result(), do: 0x0C
|
||||
def lp_check_duplicated_id_result(), do: 0x0D
|
||||
def lp_create_new_character_result(), do: 0x0E
|
||||
def lp_delete_character_result(), do: 0x0F
|
||||
def lp_migrate_command(), do: 0x10
|
||||
def lp_alive_req(), do: 0x11
|
||||
def lp_latest_connected_world(), do: 0x12
|
||||
def lp_recommend_world_message(), do: 0x13
|
||||
def lp_check_spw_result(), do: 0x14
|
||||
def lp_security_packet(), do: 0x15
|
||||
def lp_permission_request(), do: 0x16
|
||||
def lp_exception_log(), do: 0x17
|
||||
def lp_set_security_event(), do: 0x18
|
||||
def lp_set_client_key(), do: 0x19
|
||||
|
||||
# Character/Map Operations
|
||||
def lp_spawn_player(), do: 184
|
||||
def lp_remove_player_from_map(), do: 185
|
||||
def lp_chattext(), do: 186
|
||||
def lp_move_player(), do: 226
|
||||
def lp_update_char_look(), do: 241
|
||||
def lp_whisper(), do: 0x9B
|
||||
def lp_multi_chat(), do: 0x9C
|
||||
def lp_set_physical_office_ip_addr(), do: 0x1A
|
||||
def lp_end_of_scheck(), do: 0x1B
|
||||
|
||||
# NPC Operations
|
||||
def lp_npc_action(), do: 0x159
|
||||
def lp_npc_talk(), do: 0x1A3
|
||||
def lp_open_npc_shop(), do: 0x1A5
|
||||
def lp_confirm_shop_transaction(), do: 0x1A6
|
||||
def lp_open_storage(), do: 0x1A9
|
||||
|
||||
# ==================================================================================================
|
||||
# Helper Functions
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Looks up an opcode name by its value.
|
||||
"""
|
||||
def find_by_value(value) when is_integer(value) do
|
||||
# This would need to be generated from the opcodes above
|
||||
# For now, return the hex value
|
||||
"0x#{Integer.to_string(value, 16) |> String.upcase() |> String.pad_leading(2, "0")}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all client packet opcodes as a map.
|
||||
"""
|
||||
def client_opcodes do
|
||||
%{
|
||||
cp_client_hello: 0x01,
|
||||
cp_check_password: 0x02,
|
||||
cp_world_info_request: 0x04,
|
||||
cp_select_world: 0x05,
|
||||
cp_check_user_limit: 0x06,
|
||||
cp_migrate_in: 0x0D,
|
||||
cp_check_duplicated_id: 0x0E,
|
||||
cp_create_new_character: 0x12,
|
||||
cp_create_ultimate: 0x14,
|
||||
cp_delete_character: 0x15,
|
||||
cp_alive_ack: 0x16,
|
||||
cp_exception_log: 0x17,
|
||||
cp_security_packet: 0x18,
|
||||
cp_select_character: 0x19,
|
||||
cp_check_spw_request: 0x1A,
|
||||
cp_client_dump_log: 0x1D,
|
||||
cp_create_security_handle: 0x1E,
|
||||
cp_rsa_key: 0x20,
|
||||
cp_hardware_info: 0x21,
|
||||
cp_set_code_page: 0x22,
|
||||
cp_window_focus: 0x23,
|
||||
cp_inject_packet: 0x24,
|
||||
cp_change_channel: 0x25,
|
||||
cp_enter_cash_shop: 0x26,
|
||||
cp_enter_mts: 0x27,
|
||||
cp_enter_pvp: 0x28,
|
||||
cp_enter_pvp_party: 0x29,
|
||||
cp_leave_pvp: 0x2A,
|
||||
cp_move_player: 0x2D,
|
||||
cp_cancel_chair: 0x2F,
|
||||
cp_use_chair: 0x30,
|
||||
cp_close_range_attack: 0x32,
|
||||
cp_ranged_attack: 0x33,
|
||||
cp_magic_attack: 0x34,
|
||||
cp_passive_energy: 0x35,
|
||||
cp_take_damage: 0x37,
|
||||
cp_general_chat: 0x39,
|
||||
cp_close_chalkboard: 0x3A,
|
||||
cp_face_expression: 0x3B,
|
||||
cp_face_android: 0x3C,
|
||||
cp_use_item_effect: 0x3D,
|
||||
cp_npc_talk: 0x42,
|
||||
cp_npc_move: 0x43,
|
||||
cp_npc_talk_more: 0x44,
|
||||
cp_npc_shop: 0x45,
|
||||
cp_storage: 0x46,
|
||||
cp_use_hired_merchant: 0x47,
|
||||
cp_merch_item_store: 0x49,
|
||||
cp_duey_action: 0x4A,
|
||||
cp_owl: 0x4B,
|
||||
cp_owl_warp: 0x4C,
|
||||
cp_item_sort: 0x4F,
|
||||
cp_item_gather: 0x50,
|
||||
cp_item_move: 0x51,
|
||||
cp_move_bag: 0x52,
|
||||
cp_switch_bag: 0x53,
|
||||
cp_item_pickup: 0x54,
|
||||
cp_use_item: 0x55,
|
||||
cp_cancel_item_effect: 0x56,
|
||||
cp_use_summon_bag: 0x58,
|
||||
cp_pet_food: 0x59,
|
||||
cp_use_mount_food: 0x5A,
|
||||
cp_use_scripted_npc_item: 0x5B,
|
||||
cp_use_recipe: 0x5C,
|
||||
cp_use_cash_item: 0x5D,
|
||||
cp_use_catch_item: 0x5F,
|
||||
cp_use_skill_book: 0x60,
|
||||
cp_use_familiar: 0x61,
|
||||
cp_use_owl_minerva: 0x62,
|
||||
cp_use_tele_rock: 0x6A,
|
||||
cp_use_return_scroll: 0x64,
|
||||
cp_use_upgrade_scroll: 0x65,
|
||||
cp_use_flag_scroll: 0x66,
|
||||
cp_use_equip_scroll: 0x67,
|
||||
cp_use_potential_scroll: 0x68,
|
||||
cp_use_bag: 0x6A,
|
||||
cp_use_magnify_glass: 0x6B,
|
||||
cp_distribute_ap: 0x68,
|
||||
cp_auto_assign_ap: 0x69,
|
||||
cp_heal_over_time: 0x6A,
|
||||
cp_distribute_sp: 0x6C,
|
||||
cp_special_move: 0x6D,
|
||||
cp_cancel_buff: 0x6E,
|
||||
cp_skill_effect: 0x6F,
|
||||
cp_meso_drop: 0x73,
|
||||
cp_give_fame: 0x74,
|
||||
cp_char_info_request: 0x78,
|
||||
cp_spawn_pet: 0x79,
|
||||
cp_cancel_debuff: 0x70,
|
||||
cp_change_map_special: 0x38,
|
||||
cp_use_inner_portal: 0x3A,
|
||||
cp_trock_add_map: 0x3B,
|
||||
cp_user_anti_macro_item_use_request: 0x7D,
|
||||
cp_user_anti_macro_skill_use_request: 0x7E,
|
||||
cp_user_anti_macro_question_result: 0x7F,
|
||||
cp_aran_combo: 0x80,
|
||||
cp_quest_action: 0x83,
|
||||
cp_skill_macro: 0x89,
|
||||
cp_reward_item: 0x8B,
|
||||
cp_item_maker: 0x8C,
|
||||
cp_repair_all: 0x8D,
|
||||
cp_repair: 0x8E,
|
||||
cp_party_operation: 0x91,
|
||||
cp_deny_party_request: 0x92,
|
||||
cp_allow_party_invite: 0x93,
|
||||
cp_expedition_operation: 0x94,
|
||||
cp_party_search_start: 0x95,
|
||||
cp_party_search_stop: 0x96,
|
||||
cp_guild_operation: 0x97,
|
||||
cp_deny_guild_request: 0x98,
|
||||
cp_alliance_operation: 0x99,
|
||||
cp_deny_alliance_request: 0x9A,
|
||||
cp_buddylist_modify: 0x9B,
|
||||
cp_messenger: 0x9C,
|
||||
cp_whisper: 0x9D,
|
||||
cp_request_family: 0x9E,
|
||||
cp_family_operation: 0x9F,
|
||||
cp_delete_junior: 0xA0,
|
||||
cp_delete_senior: 0xA1,
|
||||
cp_use_family: 0xA2,
|
||||
cp_family_precept: 0xA3,
|
||||
cp_family_summon: 0xA4,
|
||||
cp_accept_family: 0xA5,
|
||||
cp_mob_move: 0xA6,
|
||||
cp_mob_apply_ctrl: 0xA7,
|
||||
cp_mob_skill_delay_end: 0xA8,
|
||||
cp_mob_hit_by_mob: 0xA9,
|
||||
cp_mob_self_destruct: 0xAA,
|
||||
cp_mob_time_bomb_end: 0xAB,
|
||||
cp_mob_area_attack_disease: 0xAC,
|
||||
cp_mob_attack_mob: 0xAD,
|
||||
cp_mob_escort_collision: 0xAE,
|
||||
cp_mob_request_escort_info: 0xAF,
|
||||
cp_npc_damage_reactor: 0xC1,
|
||||
cp_touch_reactor: 0xC2,
|
||||
cp_public_npc: 0x9A,
|
||||
cp_buy_cs_item: 0xB6,
|
||||
cp_coupon_code: 0xB7,
|
||||
cp_cs_update: 0xB8,
|
||||
cp_touching_mts: 0xB9,
|
||||
cp_mts_tab: 0xBA,
|
||||
cp_use_door: 0xBC,
|
||||
cp_use_mech_door: 0xBD,
|
||||
cp_cygnus_summon: 0xBE,
|
||||
cp_ship_object: 0xBE,
|
||||
cp_party_chat: 0x3F,
|
||||
cp_player_interaction: 0xC0,
|
||||
cp_follow_request: 0xBF,
|
||||
cp_follow_reply: 0xC0,
|
||||
cp_ring_action: 0xC1,
|
||||
cp_summon_attack: 0xC6,
|
||||
cp_move_summon: 0xC4,
|
||||
cp_damage_summon: 0xC7,
|
||||
cp_sub_summon: 0xC8,
|
||||
cp_remove_summon: 0xC9,
|
||||
cp_move_dragon: 0xCA,
|
||||
cp_move_pet: 0xCB,
|
||||
cp_pet_chat: 0xCC,
|
||||
cp_pet_command: 0xCD,
|
||||
cp_pet_drop_pickup_request: 0xCE,
|
||||
cp_pet_auto_pot: 0xCF,
|
||||
cp_move_familiar: 0xD0,
|
||||
cp_rename_familiar: 0xD1,
|
||||
cp_spawn_familiar: 0xD2,
|
||||
cp_attack_familiar: 0xD3,
|
||||
cp_touch_familiar: 0xD4,
|
||||
cp_monster_carnival: 0xD5,
|
||||
cp_snowball: 0xDA,
|
||||
cp_coconut: 0xDB,
|
||||
cp_left_knock_back: 0xD8,
|
||||
cp_use_pot: 0xE3,
|
||||
cp_clear_pot: 0xE4,
|
||||
cp_feed_pot: 0xE5,
|
||||
cp_cure_pot: 0xE6,
|
||||
cp_reward_pot: 0xE7,
|
||||
cp_report: 0xE1,
|
||||
cp_game_poll: 0xE2,
|
||||
cp_pam_song: 0xE0,
|
||||
cp_move_android: 0x40
|
||||
}
|
||||
end
|
||||
end
|
||||
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
|
||||
413
lib/odinsea/net/processor.ex
Normal file
413
lib/odinsea/net/processor.ex
Normal file
@@ -0,0 +1,413 @@
|
||||
defmodule Odinsea.Net.Processor do
|
||||
@moduledoc """
|
||||
Central packet routing/dispatch system.
|
||||
Ported from Java PacketProcessor.java
|
||||
|
||||
Routes incoming packets to appropriate handlers based on:
|
||||
- Server type (Login, Channel, Shop)
|
||||
- Packet opcode
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Net.Packet.In
|
||||
alias Odinsea.Net.Opcodes
|
||||
|
||||
@type server_type :: :login | :channel | :shop
|
||||
@type client_state :: map()
|
||||
|
||||
# ==================================================================================================
|
||||
# Main Entry Point
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Main packet handler entry point.
|
||||
Routes packets to appropriate server-specific handlers.
|
||||
|
||||
## Parameters
|
||||
- `opcode` - The packet opcode (integer)
|
||||
- `packet` - The InPacket struct (already read past opcode)
|
||||
- `client_state` - The client's state map
|
||||
- `server_type` - :login | :channel | :shop
|
||||
|
||||
## Returns
|
||||
- `{:ok, new_state}` - Success with updated state
|
||||
- `{:error, reason, state}` - Error with reason
|
||||
- `{:disconnect, reason}` - Client should be disconnected
|
||||
"""
|
||||
def handle(opcode, %In{} = packet, client_state, server_type) do
|
||||
# Pre-process common packets that apply to all server types
|
||||
case preprocess(opcode, packet, client_state) do
|
||||
{:handled, new_state} ->
|
||||
{:ok, new_state}
|
||||
|
||||
:continue ->
|
||||
# Route to server-specific handler
|
||||
case server_type do
|
||||
:login ->
|
||||
handle_login(opcode, packet, client_state)
|
||||
|
||||
:shop ->
|
||||
handle_shop(opcode, packet, client_state)
|
||||
|
||||
:channel ->
|
||||
handle_channel(opcode, packet, client_state)
|
||||
|
||||
_ ->
|
||||
Logger.warning("Unknown server type: #{inspect(server_type)}")
|
||||
{:ok, client_state}
|
||||
end
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Packet processing error: #{inspect(e)}")
|
||||
{:error, :processing_error, client_state}
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Pre-Processing (Common Packets)
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Pre-processes packets that are common across all server types.
|
||||
Returns {:handled, state} if the packet was fully handled.
|
||||
Returns :continue if the packet should be routed to server-specific handlers.
|
||||
"""
|
||||
# Define opcodes as module attributes for use in guards
|
||||
@cp_security_packet Opcodes.cp_security_packet()
|
||||
@cp_alive_ack Opcodes.cp_alive_ack()
|
||||
@cp_client_dump_log Opcodes.cp_client_dump_log()
|
||||
@cp_hardware_info Opcodes.cp_hardware_info()
|
||||
@cp_inject_packet Opcodes.cp_inject_packet()
|
||||
@cp_set_code_page Opcodes.cp_set_code_page()
|
||||
@cp_window_focus Opcodes.cp_window_focus()
|
||||
@cp_exception_log Opcodes.cp_exception_log()
|
||||
|
||||
defp preprocess(opcode, packet, state) do
|
||||
case opcode do
|
||||
# Security packet - always handled in pre-process
|
||||
@cp_security_packet ->
|
||||
handle_security_packet(packet, state)
|
||||
{:handled, state}
|
||||
|
||||
# Alive acknowledgement - keep connection alive
|
||||
@cp_alive_ack ->
|
||||
handle_alive_ack(state)
|
||||
{:handled, state}
|
||||
|
||||
# Client dump log - debugging/crash reports
|
||||
@cp_client_dump_log ->
|
||||
handle_dump_log(packet, state)
|
||||
{:handled, state}
|
||||
|
||||
# Hardware info - client machine identification
|
||||
@cp_hardware_info ->
|
||||
handle_hardware_info(packet, state)
|
||||
{:handled, state}
|
||||
|
||||
# Packet injection attempt - security violation
|
||||
@cp_inject_packet ->
|
||||
handle_inject_packet(packet, state)
|
||||
{:disconnect, :packet_injection}
|
||||
|
||||
# Code page settings
|
||||
@cp_set_code_page ->
|
||||
handle_code_page(packet, state)
|
||||
{:handled, state}
|
||||
|
||||
# Window focus - anti-cheat detection
|
||||
@cp_window_focus ->
|
||||
handle_window_focus(packet, state)
|
||||
{:handled, state}
|
||||
|
||||
# Exception log from client
|
||||
@cp_exception_log ->
|
||||
handle_exception_log(packet, state)
|
||||
{:handled, state}
|
||||
|
||||
# Not a common packet, continue to server-specific handling
|
||||
_ ->
|
||||
:continue
|
||||
end
|
||||
end
|
||||
|
||||
# Login opcodes as module attributes
|
||||
@cp_client_hello Opcodes.cp_client_hello()
|
||||
@cp_check_password Opcodes.cp_check_password()
|
||||
@cp_world_info_request Opcodes.cp_world_info_request()
|
||||
@cp_select_world Opcodes.cp_select_world()
|
||||
@cp_check_user_limit Opcodes.cp_check_user_limit()
|
||||
@cp_check_duplicated_id Opcodes.cp_check_duplicated_id()
|
||||
@cp_create_new_character Opcodes.cp_create_new_character()
|
||||
@cp_create_ultimate Opcodes.cp_create_ultimate()
|
||||
@cp_delete_character Opcodes.cp_delete_character()
|
||||
@cp_select_character Opcodes.cp_select_character()
|
||||
@cp_check_spw_request Opcodes.cp_check_spw_request()
|
||||
@cp_rsa_key Opcodes.cp_rsa_key()
|
||||
|
||||
# ==================================================================================================
|
||||
# Login Server Packet Handlers
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Routes packets for the Login server.
|
||||
Delegates to Odinsea.Login.Handler module.
|
||||
"""
|
||||
defp handle_login(opcode, packet, state) do
|
||||
alias Odinsea.Login.Handler
|
||||
|
||||
case opcode do
|
||||
# Permission request (client hello / initial handshake)
|
||||
@cp_client_hello ->
|
||||
Handler.on_permission_request(packet, state)
|
||||
|
||||
# Password check (login authentication)
|
||||
@cp_check_password ->
|
||||
Handler.on_check_password(packet, state)
|
||||
|
||||
# World info request (server list)
|
||||
@cp_world_info_request ->
|
||||
Handler.on_world_info_request(state)
|
||||
|
||||
# Select world
|
||||
@cp_select_world ->
|
||||
Handler.on_select_world(packet, state)
|
||||
|
||||
# Check user limit (channel population check)
|
||||
@cp_check_user_limit ->
|
||||
Handler.on_check_user_limit(state)
|
||||
|
||||
# Check duplicated ID (character name availability)
|
||||
@cp_check_duplicated_id ->
|
||||
Handler.on_check_duplicated_id(packet, state)
|
||||
|
||||
# Create new character
|
||||
@cp_create_new_character ->
|
||||
Handler.on_create_new_character(packet, state)
|
||||
|
||||
# Create ultimate (Cygnus Knights)
|
||||
@cp_create_ultimate ->
|
||||
Handler.on_create_ultimate(packet, state)
|
||||
|
||||
# Delete character
|
||||
@cp_delete_character ->
|
||||
Handler.on_delete_character(packet, state)
|
||||
|
||||
# Select character (enter game)
|
||||
@cp_select_character ->
|
||||
Handler.on_select_character(packet, state)
|
||||
|
||||
# Second password check
|
||||
@cp_check_spw_request ->
|
||||
Handler.on_check_spw_request(packet, state)
|
||||
|
||||
# RSA key request
|
||||
@cp_rsa_key ->
|
||||
Handler.on_rsa_key(packet, state)
|
||||
|
||||
# Unhandled login packet
|
||||
_ ->
|
||||
Logger.debug("Unhandled login packet: opcode=0x#{Integer.to_string(opcode, 16)}")
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Channel opcodes as module attributes
|
||||
@cp_migrate_in Opcodes.cp_migrate_in()
|
||||
@cp_move_player Opcodes.cp_move_player()
|
||||
@cp_general_chat Opcodes.cp_general_chat()
|
||||
@cp_change_map Opcodes.cp_change_map()
|
||||
|
||||
# ==================================================================================================
|
||||
# Channel Server Packet Handlers
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Routes packets for the Channel server (game world).
|
||||
Delegates to appropriate handler modules.
|
||||
"""
|
||||
defp handle_channel(opcode, packet, state) do
|
||||
# TODO: Implement channel packet routing
|
||||
# Will route to:
|
||||
# - Odinsea.Channel.Handler.Player (movement, attacks, skills)
|
||||
# - Odinsea.Channel.Handler.Inventory (items, equipment)
|
||||
# - Odinsea.Channel.Handler.Mob (monster interactions)
|
||||
# - Odinsea.Channel.Handler.NPC (NPC dialogs, shops)
|
||||
# - Odinsea.Channel.Handler.Chat (chat, party, guild)
|
||||
# - etc.
|
||||
|
||||
case opcode do
|
||||
# Migrate in from login server
|
||||
@cp_migrate_in ->
|
||||
{character_id, _} = In.decode_int(packet)
|
||||
handle_migrate_in(character_id, state)
|
||||
|
||||
# Player movement
|
||||
@cp_move_player ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# General chat
|
||||
@cp_general_chat ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# Change map
|
||||
@cp_change_map ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# Unhandled channel packet
|
||||
_ ->
|
||||
Logger.debug("Unhandled channel packet: opcode=0x#{Integer.to_string(opcode, 16)}")
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
# Shop opcodes as module attributes
|
||||
@cp_buy_cs_item Opcodes.cp_buy_cs_item()
|
||||
@cp_coupon_code Opcodes.cp_coupon_code()
|
||||
@cp_cs_update Opcodes.cp_cs_update()
|
||||
|
||||
# ==================================================================================================
|
||||
# Cash Shop Server Packet Handlers
|
||||
# ==================================================================================================
|
||||
|
||||
@doc """
|
||||
Routes packets for the Cash Shop server.
|
||||
Delegates to Odinsea.Shop.Handler module.
|
||||
"""
|
||||
defp handle_shop(opcode, packet, state) do
|
||||
# TODO: Implement cash shop packet routing
|
||||
|
||||
case opcode do
|
||||
# Migrate in from channel server
|
||||
@cp_migrate_in ->
|
||||
{character_id, _} = In.decode_int(packet)
|
||||
handle_migrate_in(character_id, state)
|
||||
|
||||
# Buy cash item
|
||||
@cp_buy_cs_item ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# Coupon code
|
||||
@cp_coupon_code ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# Cash shop update
|
||||
@cp_cs_update ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# Leave cash shop
|
||||
@cp_change_map ->
|
||||
{:ok, state} # TODO: Implement
|
||||
|
||||
# Unhandled shop packet
|
||||
_ ->
|
||||
Logger.debug("Unhandled shop packet: opcode=0x#{Integer.to_string(opcode, 16)}")
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
# ==================================================================================================
|
||||
# Common Packet Implementations
|
||||
# ==================================================================================================
|
||||
|
||||
defp handle_security_packet(_packet, _state) do
|
||||
# Security packet - just acknowledge
|
||||
# In the Java version, this is a no-op that returns true in preProcess
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_alive_ack(state) do
|
||||
# Update last alive time
|
||||
new_state = Map.put(state, :last_alive, System.system_time(:millisecond))
|
||||
Logger.debug("Alive ack received from #{state.ip}")
|
||||
{:ok, new_state}
|
||||
end
|
||||
|
||||
defp handle_dump_log(packet, state) do
|
||||
{call_type, packet} = In.decode_short(packet)
|
||||
{error_code, packet} = In.decode_int(packet)
|
||||
{backup_buffer_size, packet} = In.decode_short(packet)
|
||||
{raw_seq, packet} = In.decode_int(packet)
|
||||
{p_type, packet} = In.decode_short(packet)
|
||||
{backup_buffer, _packet} = In.decode_buffer(packet, backup_buffer_size - 6)
|
||||
|
||||
log_msg = "[ClientDumpLog] RawSeq: #{raw_seq} CallType: #{call_type} " <>
|
||||
"ErrorCode: #{error_code} BufferSize: #{backup_buffer_size} " <>
|
||||
"Type: 0x#{Integer.to_string(p_type, 16)} " <>
|
||||
"Packet: #{inspect(backup_buffer, limit: :infinity)}"
|
||||
|
||||
Logger.warning(log_msg)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_hardware_info(packet, state) do
|
||||
{hardware_info, _packet} = In.decode_string(packet)
|
||||
new_state = Map.put(state, :hardware_info, hardware_info)
|
||||
Logger.debug("Hardware info: #{hardware_info}")
|
||||
{:ok, new_state}
|
||||
end
|
||||
|
||||
defp handle_inject_packet(packet, state) do
|
||||
start = packet.position
|
||||
finish = byte_size(packet.data) - 2
|
||||
|
||||
# Read opcode at end
|
||||
packet = %{packet | position: finish}
|
||||
{opcode, _packet} = In.decode_short(packet)
|
||||
|
||||
# Get hex string of injected packet
|
||||
injected_data = binary_part(packet.data, start, finish - start)
|
||||
|
||||
player_name = Map.get(state, :character_name, "<none>")
|
||||
player_id = Map.get(state, :character_id, 0)
|
||||
|
||||
Logger.error(
|
||||
"[InjectPacket] [Session #{state.ip}] [Player #{player_name} - #{player_id}] " <>
|
||||
"[OpCode 0x#{Integer.to_string(opcode, 16)}] #{inspect(injected_data, limit: :infinity)}"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_code_page(packet, state) do
|
||||
{code_page, packet} = In.decode_int(packet)
|
||||
{code_page_read, _packet} = In.decode_int(packet)
|
||||
|
||||
new_state =
|
||||
state
|
||||
|> Map.put(:code_page, code_page)
|
||||
|> Map.put(:code_page_read, code_page_read)
|
||||
|
||||
Logger.debug("Code page set: #{code_page}, read: #{code_page_read}")
|
||||
{:ok, new_state}
|
||||
end
|
||||
|
||||
defp handle_window_focus(packet, state) do
|
||||
{focus, _packet} = In.decode_byte(packet)
|
||||
|
||||
if focus == 0 do
|
||||
player_name = Map.get(state, :character_name, "<none>")
|
||||
player_id = Map.get(state, :character_id, 0)
|
||||
|
||||
Logger.warning(
|
||||
"[WindowFocus] Client lost focus [Session #{state.ip}] [Player #{player_name} - #{player_id}]"
|
||||
)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_exception_log(packet, state) do
|
||||
{exception_msg, _packet} = In.decode_string(packet)
|
||||
|
||||
Logger.warning("[ClientExceptionLog] [Session #{state.ip}] #{exception_msg}")
|
||||
:ok
|
||||
end
|
||||
|
||||
defp handle_migrate_in(character_id, state) do
|
||||
# TODO: Load character from database, restore session
|
||||
Logger.info("Migrate in: character_id=#{character_id}")
|
||||
new_state = Map.put(state, :character_id, character_id)
|
||||
{:ok, new_state}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user