222 lines
7.4 KiB
Elixir
222 lines
7.4 KiB
Elixir
defmodule Odinsea.Channel.Handler.InterServer do
|
|
@moduledoc """
|
|
Inter-server migration handler.
|
|
Handles players migrating into the channel server from login or other channels.
|
|
|
|
Ported from Java handling.channel.handler.InterServerHandler.MigrateIn
|
|
"""
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.Database.Context
|
|
alias Odinsea.World.Migration
|
|
alias Odinsea.Channel.Packets
|
|
|
|
@doc """
|
|
Handles character migration into the channel server.
|
|
|
|
## Parameters
|
|
- character_id: The character ID migrating in
|
|
- client_state: The client connection state
|
|
|
|
## Returns
|
|
- {:ok, new_state} on success
|
|
- {:error, reason, state} on failure
|
|
- {:disconnect, reason} on critical failure
|
|
"""
|
|
def migrate_in(character_id, %{socket: socket, ip: ip} = state) do
|
|
Logger.info("Migrate in: character_id=#{character_id} from #{ip}")
|
|
|
|
# Check if character is already online in this channel
|
|
if Odinsea.Channel.Players.is_online?(character_id) do
|
|
Logger.error("Character #{character_id} already online, disconnecting")
|
|
{:disconnect, :already_online}
|
|
else
|
|
# Check for pending migration token
|
|
token = Migration.get_pending_character(character_id)
|
|
|
|
if token do
|
|
# Validate the token
|
|
case Migration.validate_migration_token(token.id, character_id, :channel) do
|
|
{:ok, valid_token} ->
|
|
# Use transfer data if available
|
|
do_migrate_in(character_id, valid_token.account_id, state, valid_token.character_data)
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Migration token validation failed: #{inspect(reason)}")
|
|
# Fall back to database load
|
|
do_migrate_in(character_id, nil, state, %{})
|
|
end
|
|
else
|
|
# No token, load directly from database (direct login)
|
|
do_migrate_in(character_id, nil, state, %{})
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Handles channel change request from client.
|
|
|
|
## Parameters
|
|
- target_channel: The target channel (1-20)
|
|
- state: Client state
|
|
|
|
## Returns
|
|
- {:ok, new_state} - Will disconnect client for migration
|
|
"""
|
|
def change_channel(target_channel, %{character_id: char_id, account_id: acc_id} = state) do
|
|
Logger.info("Change channel: character=#{char_id} to channel #{target_channel}")
|
|
|
|
# Check server capacity
|
|
if Migration.pending_count() >= 10 do
|
|
Logger.warning("Server busy, rejecting channel change")
|
|
response = Packets.server_blocked(2)
|
|
send_packet(state, response)
|
|
send_packet(state, Packets.enable_actions())
|
|
{:ok, state}
|
|
else
|
|
# TODO: Check if player has blocked inventory, is in event, etc.
|
|
|
|
# Save character to database
|
|
Context.update_character_position(char_id, state.map_id, state.spawn_point)
|
|
|
|
# Create migration token
|
|
character_data = %{
|
|
map_id: state.map_id,
|
|
hp: state.hp,
|
|
mp: state.mp,
|
|
buffs: state.buffs || []
|
|
}
|
|
|
|
case Migration.create_migration_token(char_id, acc_id, :channel, target_channel, character_data) do
|
|
{:ok, token_id} ->
|
|
# Get channel IP and port
|
|
channel_ip = get_channel_ip(target_channel)
|
|
channel_port = get_channel_port(target_channel)
|
|
|
|
# Send migration command
|
|
response = Packets.get_channel_change(channel_ip, channel_port, char_id)
|
|
send_packet(state, response)
|
|
|
|
# Update login state
|
|
Context.update_login_state(acc_id, 3, state.ip) # CHANGE_CHANNEL
|
|
|
|
# Remove player from current channel storage
|
|
Odinsea.Channel.Players.remove_player(char_id)
|
|
|
|
# Disconnect will happen after packet is sent
|
|
{:disconnect, :changing_channel}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to create migration token: #{inspect(reason)}")
|
|
send_packet(state, Packets.server_blocked(2))
|
|
send_packet(state, Packets.enable_actions())
|
|
{:ok, state}
|
|
end
|
|
end
|
|
end
|
|
|
|
# ==================================================================================================
|
|
# Private Functions
|
|
# ==================================================================================================
|
|
|
|
defp do_migrate_in(character_id, account_id, state, transfer_data) do
|
|
# Load character from database
|
|
character = Context.load_character(character_id)
|
|
|
|
if is_nil(character) do
|
|
Logger.error("Character #{character_id} not found in database")
|
|
{:disconnect, :character_not_found}
|
|
else
|
|
# Verify account ownership if account_id provided
|
|
if account_id && character.accountid != account_id do
|
|
Logger.error("Character account mismatch: expected #{account_id}, got #{character.accountid}")
|
|
{:disconnect, :account_mismatch}
|
|
else
|
|
# Check login state
|
|
login_state = Context.get_login_state(character.accountid)
|
|
|
|
allow_login =
|
|
login_state in [0, 1, 3] # NOTLOGGEDIN, SERVER_TRANSITION, or CHANGE_CHANNEL
|
|
# TODO: Check if character is already connected on another account's session
|
|
|
|
if allow_login do
|
|
complete_migration(character, state, transfer_data)
|
|
else
|
|
Logger.warning("Character #{character_id} already logged in elsewhere")
|
|
{:disconnect, :already_logged_in}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
defp complete_migration(character, state, transfer_data) do
|
|
# Update login state to logged in
|
|
Context.update_login_state(character.accountid, 2, state.ip)
|
|
|
|
# Add to channel player storage
|
|
:ok = Odinsea.Channel.Players.add_player(character.id, %{
|
|
character_id: character.id,
|
|
account_id: character.accountid,
|
|
name: character.name,
|
|
map_id: character.map,
|
|
level: character.level,
|
|
job: character.job,
|
|
socket: state.socket
|
|
})
|
|
|
|
# Restore buffs/cooldowns from transfer data or storage
|
|
restored_buffs = transfer_data[:buffs] || []
|
|
|
|
# Send character info packet
|
|
char_info = Packets.get_char_info(character, restored_buffs)
|
|
send_packet(state, char_info)
|
|
|
|
# Send cash shop enable packet
|
|
send_packet(state, Packets.enable_cash_shop())
|
|
|
|
# TODO: Send buddy list, guild info, etc.
|
|
|
|
new_state =
|
|
state
|
|
|> Map.put(:character_id, character.id)
|
|
|> Map.put(:account_id, character.accountid)
|
|
|> Map.put(:character_name, character.name)
|
|
|> Map.put(:map_id, character.map)
|
|
|> Map.put(:hp, character.hp)
|
|
|> Map.put(:mp, character.mp)
|
|
|> Map.put(:level, character.level)
|
|
|> Map.put(:job, character.job)
|
|
|> Map.put(:logged_in, true)
|
|
|
|
Logger.info("Character #{character.name} (#{character.id}) successfully migrated in")
|
|
|
|
{:ok, new_state}
|
|
end
|
|
|
|
defp send_packet(%{socket: socket}, packet_data) do
|
|
packet_length = byte_size(packet_data)
|
|
header = <<packet_length::little-size(16)>>
|
|
|
|
case :gen_tcp.send(socket, header <> packet_data) do
|
|
:ok -> :ok
|
|
{:error, reason} ->
|
|
Logger.error("Failed to send packet: #{inspect(reason)}")
|
|
:error
|
|
end
|
|
end
|
|
|
|
defp send_packet(_, _), do: :error
|
|
|
|
defp get_channel_ip(channel_id) do
|
|
# TODO: Get from configuration
|
|
Application.get_env(:odinsea, :channel_ip, "127.0.0.1")
|
|
end
|
|
|
|
defp get_channel_port(channel_id) do
|
|
# TODO: Get from configuration
|
|
base_port = Application.get_env(:odinsea, :channel_base_port, 8585)
|
|
base_port + channel_id - 1
|
|
end
|
|
end
|