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 = <> 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