576 lines
15 KiB
Elixir
576 lines
15 KiB
Elixir
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()
|
|
|
|
# Inventory opcodes
|
|
cp_item_move = Opcodes.cp_item_move()
|
|
cp_item_sort = Opcodes.cp_item_sort()
|
|
cp_item_gather = Opcodes.cp_item_gather()
|
|
cp_use_item = Opcodes.cp_use_item()
|
|
cp_use_return_scroll = Opcodes.cp_use_return_scroll()
|
|
cp_use_scroll = Opcodes.cp_use_upgrade_scroll()
|
|
cp_use_cash_item = Opcodes.cp_use_cash_item()
|
|
|
|
# NPC opcodes
|
|
cp_npc_move = Opcodes.cp_npc_move()
|
|
cp_npc_talk = Opcodes.cp_npc_talk()
|
|
cp_npc_talk_more = Opcodes.cp_npc_talk_more()
|
|
cp_npc_shop = Opcodes.cp_npc_shop()
|
|
cp_storage = Opcodes.cp_storage()
|
|
cp_quest_action = Opcodes.cp_quest_action()
|
|
cp_repair = Opcodes.cp_repair()
|
|
cp_repair_all = Opcodes.cp_repair_all()
|
|
cp_update_quest = Opcodes.cp_update_quest()
|
|
cp_use_item_quest = Opcodes.cp_use_item_quest()
|
|
cp_public_npc = Opcodes.cp_public_npc()
|
|
cp_use_scripted_npc_item = Opcodes.cp_use_scripted_npc_item()
|
|
|
|
# Mob opcodes
|
|
cp_move_life = Opcodes.cp_move_life()
|
|
cp_auto_aggro = Opcodes.cp_auto_aggro()
|
|
cp_mob_skill_delay_end = Opcodes.cp_mob_skill_delay_end()
|
|
cp_mob_bomb = Opcodes.cp_mob_bomb()
|
|
|
|
# Summon opcodes
|
|
cp_move_summon = Opcodes.cp_move_summon()
|
|
cp_summon_attack = Opcodes.cp_summon_attack()
|
|
cp_damage_summon = Opcodes.cp_damage_summon()
|
|
cp_sub_summon = Opcodes.cp_sub_summon()
|
|
cp_remove_summon = Opcodes.cp_remove_summon()
|
|
cp_move_dragon = Opcodes.cp_move_dragon()
|
|
|
|
# Player operations
|
|
cp_note_action = Opcodes.cp_note_action()
|
|
cp_give_fame = Opcodes.cp_give_fame()
|
|
cp_use_door = Opcodes.cp_use_door()
|
|
cp_use_mech_door = Opcodes.cp_use_mech_door()
|
|
cp_transform_player = Opcodes.cp_transform_player()
|
|
cp_damage_reactor = Opcodes.cp_damage_reactor()
|
|
cp_touch_reactor = Opcodes.cp_touch_reactor()
|
|
cp_coconut = Opcodes.cp_coconut()
|
|
cp_follow_request = Opcodes.cp_follow_request()
|
|
cp_follow_reply = Opcodes.cp_follow_reply()
|
|
cp_ring_action = Opcodes.cp_ring_action()
|
|
cp_solomon = Opcodes.cp_solomon()
|
|
cp_gach_exp = Opcodes.cp_gach_exp()
|
|
cp_report = Opcodes.cp_report()
|
|
cp_enter_pvp = Opcodes.cp_enter_pvp()
|
|
cp_leave_pvp = Opcodes.cp_leave_pvp()
|
|
cp_pvp_respawn = Opcodes.cp_pvp_respawn()
|
|
cp_pvp_attack = Opcodes.cp_pvp_attack()
|
|
|
|
# UI opcodes
|
|
cp_cygnus_summon = Opcodes.cp_cygnus_summon()
|
|
cp_game_poll = Opcodes.cp_game_poll()
|
|
cp_ship_object = Opcodes.cp_ship_object()
|
|
|
|
# BBS
|
|
cp_bbs_operation = Opcodes.cp_bbs_operation()
|
|
|
|
# Duey
|
|
cp_duey_action = Opcodes.cp_duey_action()
|
|
|
|
# Monster Carnival
|
|
cp_monster_carnival = Opcodes.cp_monster_carnival()
|
|
|
|
# Alliance
|
|
cp_alliance_operation = Opcodes.cp_alliance_operation()
|
|
cp_deny_alliance_request = Opcodes.cp_deny_alliance_request()
|
|
|
|
# Item Maker / Crafting
|
|
cp_item_maker = Opcodes.cp_item_maker()
|
|
cp_use_recipe = Opcodes.cp_use_recipe()
|
|
cp_make_extractor = Opcodes.cp_make_extractor()
|
|
cp_use_bag = Opcodes.cp_use_bag()
|
|
cp_start_harvest = Opcodes.cp_start_harvest()
|
|
cp_stop_harvest = Opcodes.cp_stop_harvest()
|
|
cp_profession_info = Opcodes.cp_profession_info()
|
|
cp_craft_effect = Opcodes.cp_craft_effect()
|
|
cp_craft_make = Opcodes.cp_craft_make()
|
|
cp_craft_done = Opcodes.cp_craft_done()
|
|
cp_use_pot = Opcodes.cp_use_pot()
|
|
cp_clear_pot = Opcodes.cp_clear_pot()
|
|
cp_feed_pot = Opcodes.cp_feed_pot()
|
|
cp_cure_pot = Opcodes.cp_cure_pot()
|
|
cp_reward_pot = Opcodes.cp_reward_pot()
|
|
|
|
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
|
|
|
|
# Inventory handlers
|
|
^cp_item_move ->
|
|
case Handler.Inventory.handle_item_move(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
^cp_item_sort ->
|
|
case Handler.Inventory.handle_item_sort(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
^cp_item_gather ->
|
|
case Handler.Inventory.handle_item_gather(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
^cp_use_item ->
|
|
case Handler.Inventory.handle_use_item(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
^cp_use_return_scroll ->
|
|
case Handler.Inventory.handle_use_return_scroll(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
^cp_use_scroll ->
|
|
case Handler.Inventory.handle_use_scroll(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
^cp_use_cash_item ->
|
|
case Handler.Inventory.handle_use_cash_item(packet, state) do
|
|
{:ok, new_state} -> new_state
|
|
_ -> state
|
|
end
|
|
|
|
# NPC handlers
|
|
^cp_npc_move ->
|
|
Handler.NPC.handle_npc_move(packet, self())
|
|
state
|
|
|
|
^cp_npc_talk ->
|
|
Handler.NPC.handle_npc_talk(packet, self())
|
|
state
|
|
|
|
^cp_npc_talk_more ->
|
|
Handler.NPC.handle_npc_more_talk(packet, self())
|
|
state
|
|
|
|
^cp_npc_shop ->
|
|
Handler.NPC.handle_npc_shop(packet, self())
|
|
state
|
|
|
|
^cp_storage ->
|
|
Handler.NPC.handle_storage(packet, self())
|
|
state
|
|
|
|
^cp_quest_action ->
|
|
Handler.NPC.handle_quest_action(packet, self())
|
|
state
|
|
|
|
^cp_repair ->
|
|
Handler.NPC.handle_repair(packet, self())
|
|
state
|
|
|
|
^cp_repair_all ->
|
|
Handler.NPC.handle_repair_all(self())
|
|
state
|
|
|
|
^cp_update_quest ->
|
|
Handler.NPC.handle_update_quest(packet, self())
|
|
state
|
|
|
|
^cp_use_item_quest ->
|
|
Handler.NPC.handle_use_item_quest(packet, self())
|
|
state
|
|
|
|
^cp_public_npc ->
|
|
Handler.NPC.handle_public_npc(packet, self())
|
|
state
|
|
|
|
^cp_use_scripted_npc_item ->
|
|
Handler.NPC.handle_use_scripted_npc_item(packet, self())
|
|
state
|
|
|
|
# Mob handlers
|
|
^cp_move_life ->
|
|
Handler.Mob.handle_mob_move(packet, self())
|
|
state
|
|
|
|
^cp_auto_aggro ->
|
|
Handler.Mob.handle_auto_aggro(packet, self())
|
|
state
|
|
|
|
^cp_mob_skill_delay_end ->
|
|
Handler.Mob.handle_mob_skill_delay_end(packet, self())
|
|
state
|
|
|
|
^cp_mob_bomb ->
|
|
Handler.Mob.handle_mob_bomb(packet, self())
|
|
state
|
|
|
|
# Summon handlers
|
|
^cp_move_summon ->
|
|
Handler.Summon.handle_move_summon(packet, self())
|
|
state
|
|
|
|
^cp_summon_attack ->
|
|
Handler.Summon.handle_summon_attack(packet, self())
|
|
state
|
|
|
|
^cp_damage_summon ->
|
|
Handler.Summon.handle_damage_summon(packet, self())
|
|
state
|
|
|
|
^cp_sub_summon ->
|
|
Handler.Summon.handle_sub_summon(packet, self())
|
|
state
|
|
|
|
^cp_remove_summon ->
|
|
Handler.Summon.handle_remove_summon(packet, self())
|
|
state
|
|
|
|
^cp_move_dragon ->
|
|
Handler.Summon.handle_move_dragon(packet, self())
|
|
state
|
|
|
|
# Player handlers
|
|
^cp_note_action ->
|
|
Handler.Players.handle_note(packet, self())
|
|
state
|
|
|
|
^cp_give_fame ->
|
|
Handler.Players.handle_give_fame(packet, self())
|
|
state
|
|
|
|
^cp_use_door ->
|
|
Handler.Players.handle_use_door(packet, self())
|
|
state
|
|
|
|
^cp_use_mech_door ->
|
|
Handler.Players.handle_use_mech_door(packet, self())
|
|
state
|
|
|
|
^cp_transform_player ->
|
|
Handler.Players.handle_transform_player(packet, self())
|
|
state
|
|
|
|
^cp_damage_reactor ->
|
|
Handler.Players.handle_hit_reactor(packet, self())
|
|
state
|
|
|
|
^cp_touch_reactor ->
|
|
Handler.Players.handle_touch_reactor(packet, self())
|
|
state
|
|
|
|
^cp_coconut ->
|
|
Handler.Players.handle_hit_coconut(packet, self())
|
|
state
|
|
|
|
^cp_follow_request ->
|
|
Handler.Players.handle_follow_request(packet, self())
|
|
state
|
|
|
|
^cp_follow_reply ->
|
|
Handler.Players.handle_follow_reply(packet, self())
|
|
state
|
|
|
|
^cp_ring_action ->
|
|
Handler.Players.handle_ring_action(packet, self())
|
|
state
|
|
|
|
^cp_solomon ->
|
|
Handler.Players.handle_solomon(packet, self())
|
|
state
|
|
|
|
^cp_gach_exp ->
|
|
Handler.Players.handle_gach_exp(packet, self())
|
|
state
|
|
|
|
^cp_report ->
|
|
Handler.Players.handle_report(packet, self())
|
|
state
|
|
|
|
^cp_enter_pvp ->
|
|
Handler.Players.handle_enter_pvp(packet, self())
|
|
state
|
|
|
|
^cp_leave_pvp ->
|
|
Handler.Players.handle_leave_pvp(packet, self())
|
|
state
|
|
|
|
^cp_pvp_respawn ->
|
|
Handler.Players.handle_respawn_pvp(packet, self())
|
|
state
|
|
|
|
^cp_pvp_attack ->
|
|
Handler.Players.handle_attack_pvp(packet, self())
|
|
state
|
|
|
|
# UI handlers
|
|
^cp_cygnus_summon ->
|
|
Handler.UI.handle_cygnus_summon(packet, self())
|
|
state
|
|
|
|
^cp_game_poll ->
|
|
Handler.UI.handle_game_poll(packet, self())
|
|
state
|
|
|
|
^cp_ship_object ->
|
|
Handler.UI.handle_ship_object(packet, self())
|
|
state
|
|
|
|
# BBS handler
|
|
^cp_bbs_operation ->
|
|
Handler.BBS.handle_bbs_operation(packet, self())
|
|
state
|
|
|
|
# Duey handler
|
|
^cp_duey_action ->
|
|
Handler.Duey.handle_duey_operation(packet, self())
|
|
state
|
|
|
|
# Monster Carnival handler
|
|
^cp_monster_carnival ->
|
|
Handler.MonsterCarnival.handle_monster_carnival(packet, self())
|
|
state
|
|
|
|
# Alliance handlers
|
|
^cp_alliance_operation ->
|
|
Handler.Alliance.handle_alliance(packet, self())
|
|
state
|
|
|
|
^cp_deny_alliance_request ->
|
|
Handler.Alliance.handle_deny_invite(packet, self())
|
|
state
|
|
|
|
# Item Maker handlers
|
|
^cp_item_maker ->
|
|
Handler.ItemMaker.handle_item_maker(packet, self())
|
|
state
|
|
|
|
^cp_use_recipe ->
|
|
Handler.ItemMaker.handle_use_recipe(packet, self())
|
|
state
|
|
|
|
^cp_make_extractor ->
|
|
Handler.ItemMaker.handle_make_extractor(packet, self())
|
|
state
|
|
|
|
^cp_use_bag ->
|
|
Handler.ItemMaker.handle_use_bag(packet, self())
|
|
state
|
|
|
|
^cp_start_harvest ->
|
|
Handler.ItemMaker.handle_start_harvest(packet, self())
|
|
state
|
|
|
|
^cp_stop_harvest ->
|
|
Handler.ItemMaker.handle_stop_harvest(packet, self())
|
|
state
|
|
|
|
^cp_profession_info ->
|
|
Handler.ItemMaker.handle_profession_info(packet, self())
|
|
state
|
|
|
|
^cp_craft_effect ->
|
|
Handler.ItemMaker.handle_craft_effect(packet, self())
|
|
state
|
|
|
|
^cp_craft_make ->
|
|
Handler.ItemMaker.handle_craft_make(packet, self())
|
|
state
|
|
|
|
^cp_craft_done ->
|
|
Handler.ItemMaker.handle_craft_complete(packet, self())
|
|
state
|
|
|
|
^cp_use_pot ->
|
|
Handler.ItemMaker.handle_use_pot(packet, self())
|
|
state
|
|
|
|
^cp_clear_pot ->
|
|
Handler.ItemMaker.handle_clear_pot(packet, self())
|
|
state
|
|
|
|
^cp_feed_pot ->
|
|
Handler.ItemMaker.handle_feed_pot(packet, self())
|
|
state
|
|
|
|
^cp_cure_pot ->
|
|
Handler.ItemMaker.handle_cure_pot(packet, self())
|
|
state
|
|
|
|
^cp_reward_pot ->
|
|
Handler.ItemMaker.handle_reward_pot(packet, self())
|
|
state
|
|
|
|
_ ->
|
|
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
|