201 lines
5.3 KiB
Elixir
201 lines
5.3 KiB
Elixir
defmodule Odinsea.Shop.Client do
|
|
@moduledoc """
|
|
Client connection handler for the cash shop server.
|
|
|
|
Handles:
|
|
- Cash shop operations (buy, gift, wishlist, etc.)
|
|
- MTS (Maple Trading System) operations
|
|
- Coupon redemption
|
|
- Inventory management
|
|
"""
|
|
|
|
use GenServer, restart: :temporary
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.Net.Packet.In
|
|
alias Odinsea.Net.Opcodes
|
|
alias Odinsea.Shop.{Operation, MTS, Packets}
|
|
alias Odinsea.Database.Context
|
|
|
|
defstruct [
|
|
:socket,
|
|
:ip,
|
|
:state,
|
|
:character_id,
|
|
:account_id,
|
|
:character,
|
|
:account
|
|
]
|
|
|
|
def start_link(socket) do
|
|
GenServer.start_link(__MODULE__, socket)
|
|
end
|
|
|
|
@impl true
|
|
def init(socket) do
|
|
{:ok, {ip, _port}} = :inet.peername(socket)
|
|
ip_string = format_ip(ip)
|
|
|
|
Logger.info("Cash shop client connected from #{ip_string}")
|
|
|
|
state = %__MODULE__{
|
|
socket: socket,
|
|
ip: ip_string,
|
|
state: :connected,
|
|
character_id: nil,
|
|
account_id: nil,
|
|
character: nil,
|
|
account: 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("Cash shop client disconnected: #{state.ip}")
|
|
{:stop, :normal, state}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Cash shop 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
|
|
|
|
# ==============================================================================
|
|
# Packet Handling
|
|
# ==============================================================================
|
|
|
|
defp handle_packet(data, state) do
|
|
packet = In.new(data)
|
|
|
|
case In.decode_short(packet) do
|
|
{opcode, packet} ->
|
|
Logger.debug("Cash shop 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
|
|
cond do
|
|
opcode == Opcodes.cp_player_loggedin() ->
|
|
handle_migrate_in(packet, state)
|
|
|
|
opcode == Opcodes.cp_cs_update() ->
|
|
# Cash shop operations
|
|
handle_cash_shop_operation(packet, state)
|
|
|
|
opcode == Opcodes.cp_mts_operation() ->
|
|
# MTS operations
|
|
handle_mts_operation(packet, state)
|
|
|
|
opcode == Opcodes.cp_alive_ack() ->
|
|
# Ping response - ignore
|
|
state
|
|
|
|
true ->
|
|
Logger.debug("Unhandled cash shop opcode: 0x#{Integer.to_string(opcode, 16)}")
|
|
state
|
|
end
|
|
end
|
|
|
|
# ==============================================================================
|
|
# Migrate In Handler
|
|
# ==============================================================================
|
|
|
|
defp handle_migrate_in(packet, state) do
|
|
{char_id, packet} = In.decode_int(packet)
|
|
{_client_ip, _packet} = In.decode_string(packet) # Skip client IP
|
|
|
|
Logger.info("Cash shop migrate in for character #{char_id}")
|
|
|
|
# Load character and account
|
|
case Context.get_character(char_id) do
|
|
nil ->
|
|
Logger.error("Character #{char_id} not found")
|
|
:gen_tcp.close(state.socket)
|
|
state
|
|
|
|
character ->
|
|
case Context.get_account(character.account_id) do
|
|
nil ->
|
|
Logger.error("Account #{character.account_id} not found")
|
|
:gen_tcp.close(state.socket)
|
|
state
|
|
|
|
account ->
|
|
# Load gifts
|
|
gifts = Context.load_gifts(character.id)
|
|
character = %{character | cash_inventory: gifts ++ (character.cash_inventory || [])}
|
|
|
|
# Send cash shop setup
|
|
setup_packet = Packets.set_cash_shop(character)
|
|
:gen_tcp.send(state.socket, setup_packet)
|
|
|
|
# Send initial update
|
|
Operation.cs_update(state.socket, character)
|
|
|
|
%{state |
|
|
state: :in_cash_shop,
|
|
character_id: char_id,
|
|
account_id: account.id,
|
|
character: character,
|
|
account: account
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
# ==============================================================================
|
|
# Cash Shop Operation Handler
|
|
# ==============================================================================
|
|
|
|
defp handle_cash_shop_operation(packet, state) do
|
|
# Delegate to Operation module
|
|
Operation.handle(packet, state)
|
|
end
|
|
|
|
# ==============================================================================
|
|
# MTS Operation Handler
|
|
# ==============================================================================
|
|
|
|
defp handle_mts_operation(packet, state) do
|
|
# Delegate to MTS module
|
|
MTS.handle(packet, state)
|
|
end
|
|
|
|
# ==============================================================================
|
|
# Utility Functions
|
|
# ==============================================================================
|
|
|
|
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
|