Start repo, claude & kimi still vibing tho

This commit is contained in:
ra
2026-02-14 17:04:21 -07:00
commit f5b8aeb39d
54 changed files with 9466 additions and 0 deletions

View 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