Start repo, claude & kimi still vibing tho
This commit is contained in:
221
lib/odinsea/channel/handler/inter_server.ex
Normal file
221
lib/odinsea/channel/handler/inter_server.ex
Normal file
@@ -0,0 +1,221 @@
|
||||
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
|
||||
Reference in New Issue
Block a user