defmodule Odinsea.Channel.Handler.Buddy do @moduledoc """ Handles buddy list operations. Ported from src/handling/channel/handler/BuddyListHandler.java Manages buddy list add, remove, and accept operations. """ require Logger alias Odinsea.Net.Packet.In alias Odinsea.Channel.Packets alias Odinsea.Game.Character alias Odinsea.Database.Context @max_buddy_list 100 @default_capacity 20 @doc """ Handles buddy list operations (CP_BUDDYLIST_MODIFY). Ported from BuddyListHandler.BuddyOperation() Mode: - 1: Add buddy - 2: Accept buddy - 3: Delete buddy """ def handle_buddy_operation(packet, client_state) do with {:ok, character_pid} <- get_character(client_state), {:ok, character} <- Character.get_state(character_pid) do {mode, packet} = In.decode_byte(packet) case mode do 1 -> handle_add_buddy(packet, character, client_state) 2 -> handle_accept_buddy(packet, character, client_state) 3 -> handle_delete_buddy(packet, character, client_state) _ -> Logger.warning("Unknown buddy operation mode: #{mode}") {:ok, client_state} end else {:error, reason} -> Logger.warning("Buddy operation failed: #{inspect(reason)}") {:ok, client_state} end end # ============================================================================ # Add Buddy Handler # ============================================================================ defp handle_add_buddy(packet, character, client_state) do {add_name, packet} = In.decode_string(packet) {group_name, _packet} = In.decode_string(packet) # Validate inputs if String.length(add_name) > 13 || String.length(group_name) > 16 do {:ok, client_state} else # Check if already in buddy list existing = find_buddy(character.buddies, add_name) cond do existing && existing.group == group_name -> # Already in list with same group send_buddy_message(client_state, 11) existing && !existing.pending -> # Update group updated_buddies = update_buddy_group(character.buddies, add_name, group_name) Character.update_buddies(character.id, updated_buddies) # Send updated buddy list buddy_list_packet = Packets.update_buddylist(updated_buddies, 10) send_packet(client_state, buddy_list_packet) length(character.buddies) >= @max_buddy_list -> # Buddy list full send_buddy_message(client_state, 11) true -> # Try to find and add buddy add_buddy_to_list(add_name, group_name, character, client_state) end {:ok, client_state} end end defp add_buddy_to_list(add_name, group_name, character, client_state) do # Try to find character on current channel case Odinsea.Channel.Players.find_by_name(client_state.channel_id, add_name) do {:ok, target_character} -> # Check if can add (not GM hiding, not blacklisted) if can_add_buddy?(character, target_character) do # Check target's buddy list capacity if length(target_character.buddies) >= @default_capacity do send_buddy_message(client_state, 12) else # Send buddy request to target send_buddy_request(target_character, character) # Add pending buddy to our list buddy = create_buddy_entry(target_character, group_name, -1, true) updated_buddies = character.buddies ++ [buddy] Character.update_buddies(character.id, updated_buddies) # Send updated buddy list buddy_list_packet = Packets.update_buddylist(updated_buddies, 10) send_packet(client_state, buddy_list_packet) end else send_buddy_message(client_state, 15) end {:error, :not_found} -> # Try to find in database case Context.get_character_by_name(add_name) do nil -> send_buddy_message(client_state, 15) target_db -> # Check if target can accept buddy if target_db.gm_level < 3 do # Check buddy capacity in database case get_buddy_count_from_db(target_db.id) do {:ok, count} when count >= @default_capacity -> send_buddy_message(client_state, 12) _ -> # Add pending to database insert_pending_buddy(target_db.id, character.id, group_name) # Add pending buddy to our list buddy = create_buddy_entry_from_db(target_db, group_name, true) updated_buddies = character.buddies ++ [buddy] Character.update_buddies(character.id, updated_buddies) # Send updated buddy list buddy_list_packet = Packets.update_buddylist(updated_buddies, 10) send_packet(client_state, buddy_list_packet) end else send_buddy_message(client_state, 15) end end end end # ============================================================================ # Accept Buddy Handler # ============================================================================ defp handle_accept_buddy(packet, character, client_state) do {other_cid, _packet} = In.decode_int(packet) # Find pending buddy buddy = Enum.find(character.buddies, fn b -> b.character_id == other_cid && b.pending end) if buddy && length(character.buddies) < @max_buddy_list do # Accept the buddy updated_buddy = %{buddy | pending: false, visible: true, group: "ETC"} # Update buddy in list updated_buddies = Enum.map(character.buddies, fn b -> if b.character_id == other_cid do updated_buddy else b end end) Character.update_buddies(character.id, updated_buddies) # Try to find channel channel = find_buddy_channel(other_cid) # Send updated buddy list buddy_list_packet = Packets.update_buddylist(updated_buddies, 10) send_packet(client_state, buddy_list_packet) # Notify other player if online if channel > 0 do notify_buddy_added(other_cid, character, "ETC") end # Update in database accept_buddy_in_db(character.id, other_cid) else send_buddy_message(client_state, 11) end {:ok, client_state} end # ============================================================================ # Delete Buddy Handler # ============================================================================ defp handle_delete_buddy(packet, character, client_state) do {other_cid, _packet} = In.decode_int(packet) # Find buddy buddy = Enum.find(character.buddies, fn b -> b.character_id == other_cid end) if buddy do # Notify other player if online and visible if buddy.visible do channel = find_buddy_channel(other_cid) if channel > 0 do notify_buddy_removed(other_cid, character.id) end end # Remove from our list updated_buddies = Enum.reject(character.buddies, fn b -> b.character_id == other_cid end) Character.update_buddies(character.id, updated_buddies) # Send updated buddy list buddy_list_packet = Packets.update_buddylist(updated_buddies, 18) send_packet(client_state, buddy_list_packet) # Remove from database remove_buddy_from_db(character.id, other_cid) end {:ok, client_state} end # ============================================================================ # Helper Functions # ============================================================================ defp get_character(client_state) do case client_state.character_id do nil -> {:error, :no_character} character_id -> case Registry.lookup(Odinsea.CharacterRegistry, character_id) do [{pid, _}] -> {:ok, pid} [] -> {:error, :character_not_found} end end end defp find_buddy(buddies, name) do name_lower = String.downcase(name) Enum.find(buddies, fn b -> String.downcase(b.name) == name_lower end) end defp update_buddy_group(buddies, name, group_name) do Enum.map(buddies, fn b -> if String.downcase(b.name) == String.downcase(name) do %{b | group: group_name} else b end end) end defp can_add_buddy?(character, target) do # Check if target is GM hiding if target.gm? && !character.gm? do false else # Check blacklist target_character = case Registry.lookup(Odinsea.CharacterRegistry, target.id) do [{pid, _}] -> case Character.get_state(pid) do {:ok, state} -> state _ -> nil end [] -> nil end if target_character do not Enum.member?(target_character.blacklist, String.downcase(character.name)) else true end end end defp create_buddy_entry(character, group, channel, pending) do %{ character_id: character.id, name: character.name, group: group, channel: channel, visible: !pending, pending: pending, level: character.level, job: character.job } end defp create_buddy_entry_from_db(character, group, pending) do %{ character_id: character.id, name: character.name, group: group, channel: -1, visible: !pending, pending: pending, level: character.level, job: character.job } end defp send_buddy_request(target_character, from_character) do case Registry.lookup(Odinsea.CharacterRegistry, target_character.id) do [{pid, _}] -> request_packet = Packets.request_buddylist_add( from_character.id, from_character.name, from_character.level, from_character.job ) send(pid, {:send_packet, request_packet}) [] -> :ok end end defp notify_buddy_added(target_id, from_character, group) do case Registry.lookup(Odinsea.CharacterRegistry, target_id) do [{pid, _}] -> # Add buddy entry for target buddy_entry = create_buddy_entry(from_character, group, 1, false) # Update target's buddies case Character.get_state(pid) do {:ok, target_state} -> updated_buddies = target_state.buddies ++ [buddy_entry] Character.update_buddies(target_id, updated_buddies) # Send update packet buddy_list_packet = Packets.update_buddylist(updated_buddies, 10) send(pid, {:send_packet, buddy_list_packet}) _ -> :ok end [] -> :ok end end defp notify_buddy_removed(target_id, remover_id) do case Registry.lookup(Odinsea.CharacterRegistry, target_id) do [{pid, _}] -> case Character.get_state(pid) do {:ok, target_state} -> updated_buddies = Enum.reject(target_state.buddies, fn b -> b.character_id == remover_id end) Character.update_buddies(target_id, updated_buddies) buddy_list_packet = Packets.update_buddylist(updated_buddies, 18) send(pid, {:send_packet, buddy_list_packet}) _ -> :ok end [] -> :ok end end defp find_buddy_channel(character_id) do # Try to find character on any channel # For now, just check current channel's registry case Registry.lookup(Odinsea.CharacterRegistry, character_id) do [{_pid, _}] -> 1 # Found, return channel [] -> -1 # Not found end end defp send_buddy_message(client_state, code) do packet = Packets.buddylist_message(code) send_packet(client_state, packet) end defp send_packet(client_state, packet) do if client_state.socket do :gen_tcp.send(client_state.socket, packet) end end # ============================================================================ # Database Functions (Stubs) # ============================================================================ defp get_buddy_count_from_db(character_id) do # TODO: Query buddies table for count {:ok, 0} end defp insert_pending_buddy(target_id, character_id, group_name) do # TODO: Insert pending buddy into database Logger.debug("Insert pending buddy: target=#{target_id}, from=#{character_id}, group=#{group_name}") :ok end defp accept_buddy_in_db(character_id, other_id) do # TODO: Update buddy status in database Logger.debug("Accept buddy in DB: #{character_id} <-> #{other_id}") :ok end defp remove_buddy_from_db(character_id, other_id) do # TODO: Remove buddy from database Logger.debug("Remove buddy from DB: #{character_id} X #{other_id}") :ok end end