Files
odinsea-elixir/lib/odinsea/channel/handler/chat.ex

267 lines
7.9 KiB
Elixir

defmodule Odinsea.Channel.Handler.Chat do
@moduledoc """
Handles chat packets (general, party, whisper, messenger).
Ported from src/handling/channel/handler/ChatHandler.java
"""
require Logger
alias Odinsea.Net.Packet.In
alias Odinsea.Channel.Packets
alias Odinsea.Game.Character
@max_chat_length 80
@max_staff_chat_length 512
@doc """
Handles general chat (CP_USER_CHAT).
Ported from ChatHandler.GeneralChat()
"""
def handle_general_chat(packet, client_state) do
with {:ok, character_pid} <- get_character(client_state),
{:ok, character} <- Character.get_state(character_pid),
{:ok, map_pid} <- get_map_pid(character.map_id, client_state.channel_id) do
# Decode packet
{tick, packet} = In.decode_int(packet)
{message, packet} = In.decode_string(packet)
{only_balloon, _packet} = In.decode_byte(packet)
# Validate message
cond do
String.length(message) == 0 ->
{:ok, client_state}
String.length(message) >= @max_chat_length ->
Logger.warning("Chat message too long from character #{character.id}")
{:ok, client_state}
true ->
# TODO: Process commands (CommandProcessor.processCommand)
# TODO: Check if muted
# TODO: Anti-spam checks
# Broadcast chat to map
chat_packet = Packets.user_chat(character.id, message, false, only_balloon == 1)
Odinsea.Game.Map.broadcast(map_pid, chat_packet)
# Log chat
Logger.info(
"Chat [#{character.name}] (Map #{character.map_id}): #{message}"
)
{:ok, client_state}
end
else
{:error, reason} ->
Logger.warning("General chat failed: #{inspect(reason)}")
{:ok, client_state}
end
end
@doc """
Handles party chat (CP_PARTY_CHAT).
Ported from ChatHandler.PartyChat()
Chat types:
- 0: Buddy
- 1: Party
- 2: Guild
- 3: Alliance
- 4: Expedition
"""
def handle_party_chat(packet, client_state) do
with {:ok, character_pid} <- get_character(client_state),
{:ok, character} <- Character.get_state(character_pid) do
# Decode packet
{chat_type, packet} = In.decode_byte(packet)
{num_recipients, packet} = In.decode_byte(packet)
# Validate recipients count
if num_recipients < 1 or num_recipients > 6 do
{:ok, client_state}
else
# Read recipient IDs
{recipients, packet} = decode_recipients(packet, num_recipients, [])
{message, _packet} = In.decode_string(packet)
# Validate message
if String.length(message) == 0 do
{:ok, client_state}
else
# TODO: Process commands
# TODO: Check if muted
# Route based on chat type
route_party_chat(chat_type, character, recipients, message)
# Log chat
chat_type_name = get_chat_type_name(chat_type)
Logger.info(
"Chat [#{character.name}] (#{chat_type_name}): #{message}"
)
{:ok, client_state}
end
end
else
{:error, reason} ->
Logger.warning("Party chat failed: #{inspect(reason)}")
{:ok, client_state}
end
end
@doc """
Handles whisper/find commands (CP_WHISPER).
Ported from ChatHandler.WhisperFind()
"""
def handle_whisper(packet, client_state) do
with {:ok, character_pid} <- get_character(client_state),
{:ok, character} <- Character.get_state(character_pid) do
# Decode packet
{mode, packet} = In.decode_byte(packet)
{_tick, packet} = In.decode_int(packet)
case mode do
# Find player (mode 5 or 68)
mode when mode in [5, 68] ->
{recipient, _packet} = In.decode_string(packet)
handle_find_player(recipient, character, client_state)
# Whisper (mode 6)
6 ->
{recipient, packet} = In.decode_string(packet)
{message, _packet} = In.decode_string(packet)
handle_whisper_message(recipient, message, character, client_state)
_ ->
Logger.warning("Unknown whisper mode: #{mode}")
{:ok, client_state}
end
else
{:error, reason} ->
Logger.warning("Whisper failed: #{inspect(reason)}")
{:ok, client_state}
end
end
# ============================================================================
# Private 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 get_map_pid(map_id, channel_id) do
case Registry.lookup(Odinsea.MapRegistry, {map_id, channel_id}) do
[{pid, _}] ->
{:ok, pid}
[] ->
# Map not loaded yet - load it
case DynamicSupervisor.start_child(
Odinsea.MapSupervisor,
{Odinsea.Game.Map, {map_id, channel_id}}
) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
error -> error
end
end
end
defp decode_recipients(packet, 0, acc), do: {Enum.reverse(acc), packet}
defp decode_recipients(packet, count, acc) do
{recipient_id, packet} = In.decode_int(packet)
decode_recipients(packet, count - 1, [recipient_id | acc])
end
defp route_party_chat(chat_type, character, recipients, message) do
case chat_type do
0 ->
# Buddy chat
Logger.debug("Buddy chat from #{character.name} to #{inspect(recipients)}: #{message}")
# TODO: Implement World.Buddy.buddyChat
1 ->
# Party chat
Logger.debug("Party chat from #{character.name}: #{message}")
# TODO: Implement World.Party.partyChat
2 ->
# Guild chat
Logger.debug("Guild chat from #{character.name}: #{message}")
# TODO: Implement World.Guild.guildChat
3 ->
# Alliance chat
Logger.debug("Alliance chat from #{character.name}: #{message}")
# TODO: Implement World.Alliance.allianceChat
4 ->
# Expedition chat
Logger.debug("Expedition chat from #{character.name}: #{message}")
# TODO: Implement World.Party.expedChat
_ ->
Logger.warning("Unknown party chat type: #{chat_type}")
end
end
defp get_chat_type_name(0), do: "Buddy"
defp get_chat_type_name(1), do: "Party"
defp get_chat_type_name(2), do: "Guild"
defp get_chat_type_name(3), do: "Alliance"
defp get_chat_type_name(4), do: "Expedition"
defp get_chat_type_name(_), do: "Unknown"
defp handle_find_player(recipient, character, client_state) do
# TODO: Implement player search across channels
# For now, just search locally
case Odinsea.Channel.Players.find_by_name(client_state.channel_id, recipient) do
{:ok, _target_character} ->
# Send find reply with map
# TODO: Send packet
Logger.debug("Found player #{recipient} on current channel")
{:ok, client_state}
{:error, :not_found} ->
# Search other channels
Logger.debug("Player #{recipient} not found")
# TODO: Send "player not found" packet
{:ok, client_state}
end
end
defp handle_whisper_message(recipient, message, character, client_state) do
# TODO: Check if muted
# TODO: Check blacklist
# Validate message
if String.length(message) == 0 do
{:ok, client_state}
else
# TODO: Find recipient across channels and send whisper
Logger.info("Whisper [#{character.name} -> #{recipient}]: #{message}")
# For now, just log
# TODO: Send whisper packet to recipient
# TODO: Send whisper reply packet to sender
{:ok, client_state}
end
end
end