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