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