419 lines
13 KiB
Elixir
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
|