Files
odinsea-elixir/lib/odinsea/channel/handler/buddy.ex
2026-02-14 23:12:33 -07:00

419 lines
13 KiB
Elixir

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