302 lines
8.1 KiB
Elixir
302 lines
8.1 KiB
Elixir
defmodule Odinsea.Channel.Packets do
|
|
@moduledoc """
|
|
Channel server packet builders.
|
|
Ported from Java tools.packet.MaplePacketCreator (relevant parts)
|
|
"""
|
|
|
|
alias Odinsea.Net.Packet.Out
|
|
alias Odinsea.Net.Opcodes
|
|
|
|
@doc """
|
|
Sends character information on login.
|
|
"""
|
|
def get_char_info(character, restored_buffs \\ []) do
|
|
# TODO: Full character encoding
|
|
# For now, send minimal info
|
|
Out.new(Opcodes.lp_set_field())
|
|
|> Out.encode_int(character.id)
|
|
|> Out.encode_byte(0) # Channel
|
|
|> Out.encode_byte(1) # Admin byte
|
|
|> Out.encode_byte(1) # Enabled
|
|
|> Out.encode_int(character.map)
|
|
|> Out.encode_byte(character.spawnpoint)
|
|
|> Out.encode_int(character.hp)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Enables cash shop access.
|
|
"""
|
|
def enable_cash_shop do
|
|
Out.new(Opcodes.lp_set_cash_shop_opened())
|
|
|> Out.encode_byte(1)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Server blocked message.
|
|
"""
|
|
def server_blocked(reason) do
|
|
Out.new(Opcodes.lp_server_blocked())
|
|
|> Out.encode_byte(reason)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Enable client actions.
|
|
"""
|
|
def enable_actions do
|
|
Out.new(Opcodes.lp_enable_action())
|
|
|> Out.encode_byte(0)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Channel change command.
|
|
"""
|
|
def get_channel_change(ip, port, character_id) do
|
|
ip_parts = parse_ip(ip)
|
|
|
|
Out.new(Opcodes.lp_migrate_command())
|
|
|> Out.encode_short(0) # Not cash shop
|
|
|> encode_ip(ip_parts)
|
|
|> Out.encode_short(port)
|
|
|> Out.encode_int(character_id)
|
|
|> Out.encode_bytes(<<0, 0>>)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
defp parse_ip(host) when is_binary(host) do
|
|
case String.split(host, ".") do
|
|
[a, b, c, d] ->
|
|
{
|
|
String.to_integer(a),
|
|
String.to_integer(b),
|
|
String.to_integer(c),
|
|
String.to_integer(d)
|
|
}
|
|
|
|
_ ->
|
|
{127, 0, 0, 1}
|
|
end
|
|
end
|
|
|
|
defp encode_ip(packet, {a, b, c, d}) do
|
|
packet
|
|
|> Out.encode_byte(a)
|
|
|> Out.encode_byte(b)
|
|
|> Out.encode_byte(c)
|
|
|> Out.encode_byte(d)
|
|
end
|
|
|
|
@doc """
|
|
Spawns a player on the map.
|
|
Minimal implementation - will expand with equipment, buffs, etc.
|
|
"""
|
|
def spawn_player(oid, character_state) do
|
|
# Reference: MaplePacketCreator.spawnPlayerMapobject()
|
|
# This is a minimal implementation - full version needs equipment, buffs, etc.
|
|
|
|
Out.new(Opcodes.lp_spawn_player())
|
|
|> Out.encode_int(oid)
|
|
# Damage skin (custom client feature)
|
|
# |> Out.encode_int(0)
|
|
|> Out.encode_byte(character_state.level)
|
|
|> Out.encode_string(character_state.name)
|
|
# Ultimate Explorer name (empty for now)
|
|
|> Out.encode_string("")
|
|
# Guild info (no guild for now)
|
|
|> Out.encode_int(0)
|
|
|> Out.encode_int(0)
|
|
# Buff mask (no buffs for now - TODO: implement buffs)
|
|
# For now, send minimal buff data
|
|
|> encode_buff_mask()
|
|
# Foreign buff end
|
|
|> Out.encode_short(0)
|
|
# ITEM_EFFECT
|
|
|> Out.encode_int(0)
|
|
# CHAIR
|
|
|> Out.encode_int(0)
|
|
# Position
|
|
|> Out.encode_short(character_state.position.x)
|
|
|> Out.encode_short(character_state.position.y)
|
|
|> Out.encode_byte(character_state.position.stance)
|
|
# Foothold
|
|
|> Out.encode_short(character_state.position.foothold)
|
|
# Appearance (gender, skin, face, hair)
|
|
|> Out.encode_byte(character_state.gender)
|
|
|> Out.encode_byte(character_state.skin_color)
|
|
|> Out.encode_int(character_state.face)
|
|
# Mega - shown in rankings
|
|
|> Out.encode_byte(0)
|
|
# Equipment (TODO: implement proper equipment encoding)
|
|
|> encode_appearance_minimal(character_state)
|
|
# Driver ID / passenger ID (for mounts)
|
|
|> Out.encode_int(0)
|
|
# Chalkboard text
|
|
|> Out.encode_string("")
|
|
# Ring info (3 ring slots)
|
|
|> Out.encode_int(0)
|
|
|> Out.encode_int(0)
|
|
|> Out.encode_int(0)
|
|
# Marriage ring
|
|
|> Out.encode_int(0)
|
|
# Mount info (no mount for now)
|
|
|> encode_mount_minimal()
|
|
# Player shop (none for now)
|
|
|> Out.encode_byte(0)
|
|
# Admin byte
|
|
|> Out.encode_byte(0)
|
|
# Pet info (no pets for now)
|
|
|> encode_pets_minimal()
|
|
# Taming mob (none)
|
|
|> Out.encode_int(0)
|
|
# Mini game info
|
|
|> Out.encode_byte(0)
|
|
# Chalkboard
|
|
|> Out.encode_byte(0)
|
|
# New year cards
|
|
|> Out.encode_byte(0)
|
|
# Berserk
|
|
|> Out.encode_byte(0)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Removes a player from the map.
|
|
"""
|
|
def remove_player(oid) do
|
|
Out.new(Opcodes.lp_remove_player_from_map())
|
|
|> Out.encode_int(oid)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
# ============================================================================
|
|
# Helper Functions for Spawn Encoding
|
|
# ============================================================================
|
|
|
|
defp encode_buff_mask(packet) do
|
|
# Buff mask is an array of integers representing active buffs
|
|
# For GMS v342, this is typically 14-16 integers (56-64 bytes)
|
|
# For now, send all zeros (no buffs)
|
|
packet
|
|
|> Out.encode_bytes(<<0::size(14 * 32)-little>>)
|
|
end
|
|
|
|
defp encode_appearance_minimal(packet, character) do
|
|
# Equipment encoding:
|
|
# Map of slot -> item_id
|
|
# For minimal implementation, just show hair
|
|
packet
|
|
# Equipped items map (empty for now)
|
|
|> Out.encode_byte(0)
|
|
# Masked items map (empty for now)
|
|
|> Out.encode_byte(0)
|
|
# Weapon (cash weapon)
|
|
|> Out.encode_int(0)
|
|
# Hair
|
|
|> Out.encode_int(character.hair)
|
|
# Ears (12 bit encoding for multiple items)
|
|
|> Out.encode_int(0)
|
|
end
|
|
|
|
defp encode_mount_minimal(packet) do
|
|
packet
|
|
|> Out.encode_byte(0)
|
|
# Mount level
|
|
|> Out.encode_byte(1)
|
|
# Mount exp
|
|
|> Out.encode_int(0)
|
|
# Mount fatigue
|
|
|> Out.encode_int(0)
|
|
end
|
|
|
|
defp encode_pets_minimal(packet) do
|
|
# 3 pet slots
|
|
packet
|
|
|> Out.encode_byte(0)
|
|
|> Out.encode_byte(0)
|
|
|> Out.encode_byte(0)
|
|
end
|
|
|
|
# ============================================================================
|
|
# Chat Packets
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
User chat packet.
|
|
Ported from LocalePacket.UserChat()
|
|
Reference: src/tools/packet/LocalePacket.java
|
|
"""
|
|
def user_chat(character_id, message, is_admin \\ false, only_balloon \\ false) do
|
|
Out.new(Opcodes.lp_chattext())
|
|
|> Out.encode_int(character_id)
|
|
|> Out.encode_byte(if is_admin, do: 1, else: 0)
|
|
|> Out.encode_string(message)
|
|
|> Out.encode_byte(if only_balloon, do: 1, else: 0)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Whisper chat packet (received whisper).
|
|
Ported from LocalePacket.WhisperChat()
|
|
"""
|
|
def whisper_received(sender_name, channel, message) do
|
|
Out.new(Opcodes.lp_whisper())
|
|
|> Out.encode_byte(0x12)
|
|
|> Out.encode_string(sender_name)
|
|
|> Out.encode_short(channel - 1)
|
|
|> Out.encode_string(message)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Whisper reply packet (sent whisper status).
|
|
Mode: 1 = success, 0 = failure
|
|
"""
|
|
def whisper_reply(recipient_name, mode) do
|
|
Out.new(Opcodes.lp_whisper())
|
|
|> Out.encode_byte(0x0A)
|
|
|> Out.encode_string(recipient_name)
|
|
|> Out.encode_byte(mode)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Find player reply (player found on channel).
|
|
"""
|
|
def find_player_reply(target_name, channel, is_buddy \\ false) do
|
|
Out.new(Opcodes.lp_whisper())
|
|
|> Out.encode_byte(0x09)
|
|
|> Out.encode_string(target_name)
|
|
|> Out.encode_byte(if is_buddy, do: 0x48, else: 0x01)
|
|
|> Out.encode_byte(channel)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Find player reply with map (player found on same channel).
|
|
"""
|
|
def find_player_with_map(target_name, map_id, is_buddy \\ false) do
|
|
Out.new(Opcodes.lp_whisper())
|
|
|> Out.encode_byte(0x09)
|
|
|> Out.encode_string(target_name)
|
|
|> Out.encode_byte(if is_buddy, do: 0x48, else: 0x01)
|
|
|> Out.encode_int(map_id)
|
|
|> Out.encode_bytes(<<0, 0, 0>>)
|
|
|> Out.to_data()
|
|
end
|
|
|
|
@doc """
|
|
Party chat packet.
|
|
Type: 0 = buddy, 1 = party, 2 = guild, 3 = alliance, 4 = expedition
|
|
"""
|
|
def multi_chat(sender_name, message, chat_type) do
|
|
Out.new(Opcodes.lp_multi_chat())
|
|
|> Out.encode_byte(chat_type)
|
|
|> Out.encode_string(sender_name)
|
|
|> Out.encode_string(message)
|
|
|> Out.to_data()
|
|
end
|
|
end
|