defmodule Odinsea.Channel.Client do @moduledoc """ Client connection handler for game channel servers. Manages the game session state. """ use GenServer, restart: :temporary require Logger alias Odinsea.Net.Packet.In alias Odinsea.Net.Opcodes alias Odinsea.Channel.Handler defstruct [:socket, :ip, :channel_id, :state, :character_id] def start_link({socket, channel_id}) do GenServer.start_link(__MODULE__, {socket, channel_id}) end @impl true def init({socket, channel_id}) do {:ok, {ip, _port}} = :inet.peername(socket) ip_string = format_ip(ip) Logger.info("Channel #{channel_id} client connected from #{ip_string}") state = %__MODULE__{ socket: socket, ip: ip_string, channel_id: channel_id, state: :connected, character_id: nil } send(self(), :receive) {:ok, state} end @impl true def handle_info(:receive, %{socket: socket} = state) do case :gen_tcp.recv(socket, 0, 30_000) do {:ok, data} -> new_state = handle_packet(data, state) send(self(), :receive) {:noreply, new_state} {:error, :closed} -> Logger.info("Channel #{state.channel_id} client disconnected: #{state.ip}") {:stop, :normal, state} {:error, reason} -> Logger.warning("Channel client error: #{inspect(reason)}") {:stop, :normal, state} end end @impl true def terminate(_reason, state) do if state.socket do :gen_tcp.close(state.socket) end :ok end defp handle_packet(data, state) do packet = In.new(data) case In.decode_short(packet) do {opcode, packet} -> Logger.debug("Channel #{state.channel_id} packet: opcode=0x#{Integer.to_string(opcode, 16)}") dispatch_packet(opcode, packet, state) :error -> Logger.warning("Failed to read packet opcode") state end end defp dispatch_packet(opcode, packet, state) do # Define opcodes for matching cp_general_chat = Opcodes.cp_general_chat() cp_party_chat = Opcodes.cp_party_chat() cp_whisper = Opcodes.cp_whisper() cp_move_player = Opcodes.cp_move_player() cp_change_map = Opcodes.cp_change_map() cp_change_keymap = Opcodes.cp_change_keymap() cp_skill_macro = Opcodes.cp_skill_macro() cp_close_range_attack = Opcodes.cp_close_range_attack() cp_ranged_attack = Opcodes.cp_ranged_attack() cp_magic_attack = Opcodes.cp_magic_attack() cp_take_damage = Opcodes.cp_take_damage() case opcode do # Chat handlers ^cp_general_chat -> case Handler.Chat.handle_general_chat(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_party_chat -> case Handler.Chat.handle_party_chat(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_whisper -> case Handler.Chat.handle_whisper(packet, state) do {:ok, new_state} -> new_state _ -> state end # Player movement and actions ^cp_move_player -> case Handler.Player.handle_move_player(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_change_map -> case Handler.Player.handle_change_map(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_change_keymap -> case Handler.Player.handle_change_keymap(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_skill_macro -> case Handler.Player.handle_change_skill_macro(packet, state) do {:ok, new_state} -> new_state _ -> state end # Combat handlers (stubs for now) ^cp_close_range_attack -> case Handler.Player.handle_close_range_attack(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_ranged_attack -> case Handler.Player.handle_ranged_attack(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_magic_attack -> case Handler.Player.handle_magic_attack(packet, state) do {:ok, new_state} -> new_state _ -> state end ^cp_take_damage -> case Handler.Player.handle_take_damage(packet, state) do {:ok, new_state} -> new_state _ -> state end _ -> Logger.debug("Unhandled channel opcode: 0x#{Integer.to_string(opcode, 16)}") state end end defp format_ip({a, b, c, d}) do "#{a}.#{b}.#{c}.#{d}" end defp format_ip({a, b, c, d, e, f, g, h}) do "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}" end end