port over some more
This commit is contained in:
@@ -18,6 +18,7 @@ defmodule Odinsea.Login.Handler do
|
||||
alias Odinsea.Login.Packets
|
||||
alias Odinsea.Constants.Server
|
||||
alias Odinsea.Database.Context
|
||||
alias Odinsea.Game.{JobType, InventoryType}
|
||||
|
||||
# ==================================================================================================
|
||||
# Permission Request (Client Hello / Version Check)
|
||||
@@ -78,70 +79,111 @@ defmodule Odinsea.Login.Handler do
|
||||
Logger.info("Login attempt: username=#{username} from #{state.ip}")
|
||||
|
||||
# Check if IP/MAC is banned
|
||||
# TODO: Implement IP/MAC ban checking
|
||||
is_banned = false
|
||||
ip_banned = Context.ip_banned?(state.ip)
|
||||
mac_banned = Context.mac_banned?(state.mac)
|
||||
|
||||
# Authenticate with database
|
||||
case Context.authenticate_user(username, password, state.ip) do
|
||||
{:ok, account_info} ->
|
||||
# TODO: Check if account is banned or temp banned
|
||||
# TODO: Check if already logged in (kick other session)
|
||||
if (ip_banned || mac_banned) do
|
||||
Logger.warning("Banned IP/MAC attempted login: ip=#{state.ip}, mac=#{state.mac}")
|
||||
|
||||
# If MAC banned, also ban the IP for enforcement
|
||||
if mac_banned do
|
||||
Context.ban_ip_address(state.ip, "Enforcing account ban, account #{username}", false, 4)
|
||||
end
|
||||
|
||||
response = Packets.get_login_failed(3)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
else
|
||||
# Authenticate with database
|
||||
case Context.authenticate_user(username, password, state.ip) do
|
||||
{:ok, account_info} ->
|
||||
# Check if account is banned (perm or temp)
|
||||
temp_ban_info = Context.get_temp_ban_info(account_info.account_id)
|
||||
|
||||
if temp_ban_info do
|
||||
Logger.warning("Temp banned account attempted login: #{username}")
|
||||
response = Packets.get_temp_ban(
|
||||
format_timestamp(temp_ban_info.expires),
|
||||
temp_ban_info.reason || ""
|
||||
)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
else
|
||||
# Check if already logged in - kick other session
|
||||
login_state = Context.get_login_state(account_info.account_id)
|
||||
|
||||
if login_state > 0 do
|
||||
Logger.warning("Account already logged in, kicking other session: #{username}")
|
||||
# Kick the existing session
|
||||
kick_existing_session(account_info.account_id, username)
|
||||
# Small delay to allow kick to process
|
||||
Process.sleep(100)
|
||||
end
|
||||
|
||||
# Update login state to logged in
|
||||
Context.update_login_state(account_info.account_id, 2, state.ip)
|
||||
|
||||
# Send success response
|
||||
response = Packets.get_auth_success(
|
||||
account_info.account_id,
|
||||
account_info.username,
|
||||
account_info.gender,
|
||||
account_info.is_gm,
|
||||
account_info.second_password
|
||||
)
|
||||
# Send success response
|
||||
response = Packets.get_auth_success(
|
||||
account_info.account_id,
|
||||
account_info.username,
|
||||
account_info.gender,
|
||||
account_info.is_gm,
|
||||
account_info.second_password
|
||||
)
|
||||
|
||||
new_state =
|
||||
state
|
||||
|> Map.put(:logged_in, true)
|
||||
|> Map.put(:account_id, account_info.account_id)
|
||||
|> Map.put(:account_name, account_info.username)
|
||||
|> Map.put(:gender, account_info.gender)
|
||||
|> Map.put(:is_gm, account_info.is_gm)
|
||||
|> Map.put(:second_password, account_info.second_password)
|
||||
|> Map.put(:login_attempts, 0)
|
||||
new_state =
|
||||
state
|
||||
|> Map.put(:logged_in, true)
|
||||
|> Map.put(:account_id, account_info.account_id)
|
||||
|> Map.put(:account_name, account_info.username)
|
||||
|> Map.put(:gender, account_info.gender)
|
||||
|> Map.put(:is_gm, account_info.is_gm)
|
||||
|> Map.put(:second_password, account_info.second_password)
|
||||
|> Map.put(:login_attempts, 0)
|
||||
|
||||
send_packet(state, response)
|
||||
{:ok, new_state}
|
||||
send_packet(state, response)
|
||||
{:ok, new_state}
|
||||
end
|
||||
|
||||
{:error, :invalid_credentials} ->
|
||||
# Increment login attempts
|
||||
login_attempts = Map.get(state, :login_attempts, 0) + 1
|
||||
{:error, :invalid_credentials} ->
|
||||
# Increment login attempts
|
||||
login_attempts = Map.get(state, :login_attempts, 0) + 1
|
||||
|
||||
if login_attempts > 5 do
|
||||
Logger.warning("Too many login attempts from #{state.ip}")
|
||||
{:disconnect, :too_many_attempts}
|
||||
else
|
||||
# Send login failed (reason 4 = incorrect password)
|
||||
response = Packets.get_login_failed(4)
|
||||
if login_attempts > 5 do
|
||||
Logger.warning("Too many login attempts from #{state.ip}")
|
||||
{:disconnect, :too_many_attempts}
|
||||
else
|
||||
# Send login failed (reason 4 = incorrect password)
|
||||
response = Packets.get_login_failed(4)
|
||||
send_packet(state, response)
|
||||
|
||||
new_state = Map.put(state, :login_attempts, login_attempts)
|
||||
{:ok, new_state}
|
||||
end
|
||||
|
||||
{:error, :account_not_found} ->
|
||||
# Send login failed (reason 5 = not registered ID)
|
||||
response = Packets.get_login_failed(5)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
|
||||
new_state = Map.put(state, :login_attempts, login_attempts)
|
||||
{:ok, new_state}
|
||||
end
|
||||
{:error, :already_logged_in} ->
|
||||
# Try to kick the existing session and allow retry
|
||||
Logger.warning("Already logged in, attempting to kick session for: #{username}")
|
||||
kick_existing_session_by_name(username)
|
||||
Process.sleep(100)
|
||||
|
||||
# Send login failed (reason 7 = already logged in) but client can retry
|
||||
response = Packets.get_login_failed(7)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
|
||||
{:error, :account_not_found} ->
|
||||
# Send login failed (reason 5 = not registered ID)
|
||||
response = Packets.get_login_failed(5)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
|
||||
{:error, :already_logged_in} ->
|
||||
# Send login failed (reason 7 = already logged in)
|
||||
response = Packets.get_login_failed(7)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
|
||||
{:error, :banned} ->
|
||||
# TODO: Check temp ban vs perm ban
|
||||
response = Packets.get_perm_ban(0)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
{:error, :banned} ->
|
||||
response = Packets.get_perm_ban(0)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -245,8 +287,11 @@ defmodule Odinsea.Login.Handler do
|
||||
# {:ok, state}
|
||||
# else
|
||||
|
||||
# TODO: Load character list from database
|
||||
# Load character list from database
|
||||
characters = load_characters(state.account_id, world_id)
|
||||
|
||||
# Store character IDs in state for later validation
|
||||
char_ids = Enum.map(characters, & &1.id)
|
||||
|
||||
response = Packets.get_char_list(
|
||||
characters,
|
||||
@@ -260,6 +305,7 @@ defmodule Odinsea.Login.Handler do
|
||||
state
|
||||
|> Map.put(:world, world_id)
|
||||
|> Map.put(:channel, actual_channel)
|
||||
|> Map.put(:character_ids, char_ids)
|
||||
|
||||
{:ok, new_state}
|
||||
end
|
||||
@@ -282,7 +328,7 @@ defmodule Odinsea.Login.Handler do
|
||||
else
|
||||
{char_name, _packet} = In.decode_string(packet)
|
||||
|
||||
# TODO: Check if name is forbidden or already exists
|
||||
# Check if name is forbidden or already exists
|
||||
name_used = check_name_used(char_name, state)
|
||||
|
||||
response = Packets.get_char_name_response(char_name, name_used)
|
||||
@@ -318,8 +364,8 @@ defmodule Odinsea.Login.Handler do
|
||||
{:disconnect, :not_logged_in}
|
||||
else
|
||||
{name, packet} = In.decode_string(packet)
|
||||
{job_type, packet} = In.decode_int(packet)
|
||||
{_dual_blade, packet} = In.decode_short(packet)
|
||||
{job_type_int, packet} = In.decode_int(packet)
|
||||
{dual_blade, packet} = In.decode_short(packet)
|
||||
{gender, packet} = In.decode_byte(packet)
|
||||
{face, packet} = In.decode_int(packet)
|
||||
{hair, packet} = In.decode_int(packet)
|
||||
@@ -330,17 +376,64 @@ defmodule Odinsea.Login.Handler do
|
||||
{shoes, packet} = In.decode_int(packet)
|
||||
{weapon, _packet} = In.decode_int(packet)
|
||||
|
||||
Logger.info("Create character: name=#{name}, job_type=#{job_type}")
|
||||
|
||||
# TODO: Validate appearance items
|
||||
# TODO: Create character in database
|
||||
# TODO: Add default items and quests
|
||||
|
||||
# For now, send success stub
|
||||
response = Packets.get_add_new_char_entry(%{}, false) # TODO: Pass actual character
|
||||
send_packet(state, response)
|
||||
|
||||
{:ok, state}
|
||||
Logger.info("Create character: name=#{name}, job_type=#{job_type_int}")
|
||||
|
||||
# Validate name is not forbidden and doesn't exist
|
||||
if check_name_used(name, state) do
|
||||
response = Packets.get_add_new_char_entry(nil, false)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
else
|
||||
# TODO: Validate appearance items are eligible for gender/job type
|
||||
# For now, accept the items as provided
|
||||
|
||||
# Create character with default stats
|
||||
job_type = JobType.from_int(job_type_int)
|
||||
default_stats = Context.get_default_stats_for_job(job_type_int, dual_blade)
|
||||
default_map = Context.get_default_map_for_job(job_type_int)
|
||||
|
||||
# Combine hair with hair color
|
||||
final_hair = hair + hair_color
|
||||
|
||||
# Build character attributes
|
||||
attrs = Map.merge(default_stats, %{
|
||||
name: name,
|
||||
accountid: state.account_id,
|
||||
world: state.world,
|
||||
face: face,
|
||||
hair: final_hair,
|
||||
gender: gender,
|
||||
skin: skin_color,
|
||||
map: default_map
|
||||
})
|
||||
|
||||
case Context.create_character(attrs) do
|
||||
{:ok, character} ->
|
||||
# Add default items to character inventory
|
||||
:ok = add_default_items(character.id, top, bottom, shoes, weapon, job_type_int)
|
||||
|
||||
# Add job-specific starter items and quests
|
||||
:ok = add_job_specific_starters(character.id, job_type_int)
|
||||
|
||||
Logger.info("Character created successfully: id=#{character.id}, name=#{name}")
|
||||
|
||||
# Reload character with full data
|
||||
char_data = Context.load_character(character.id)
|
||||
response = Packets.get_add_new_char_entry(char_data, true)
|
||||
send_packet(state, response)
|
||||
|
||||
# Add character ID to state's character list
|
||||
new_char_ids = [character.id | Map.get(state, :character_ids, [])]
|
||||
new_state = Map.put(state, :character_ids, new_char_ids)
|
||||
{:ok, new_state}
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Failed to create character: #{inspect(changeset.errors)}")
|
||||
response = Packets.get_add_new_char_entry(nil, false)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -366,28 +459,70 @@ defmodule Odinsea.Login.Handler do
|
||||
Handles character deletion.
|
||||
|
||||
Packet structure:
|
||||
- byte: has_spw (1 if second password provided)
|
||||
- string: second_password (if enabled)
|
||||
- string: asia_password (legacy, usually empty)
|
||||
- int: character_id
|
||||
"""
|
||||
def on_delete_character(packet, state) do
|
||||
if not Map.get(state, :logged_in, false) do
|
||||
{:disconnect, :not_logged_in}
|
||||
else
|
||||
# TODO: Read second password if enabled
|
||||
{_spw, packet} = In.decode_string(packet)
|
||||
{has_spw, packet} = In.decode_byte(packet)
|
||||
|
||||
# Read second password if enabled
|
||||
{spw, packet} =
|
||||
if has_spw > 0 do
|
||||
In.decode_string(packet)
|
||||
else
|
||||
{"", packet}
|
||||
end
|
||||
|
||||
{_asia_pw, packet} = In.decode_string(packet)
|
||||
{character_id, _packet} = In.decode_int(packet)
|
||||
|
||||
Logger.info("Delete character: character_id=#{character_id}")
|
||||
Logger.info("Delete character: character_id=#{character_id}, account=#{state.account_name}")
|
||||
|
||||
# Validate second password if account has one
|
||||
spw_valid = validate_second_password(state, spw)
|
||||
|
||||
result =
|
||||
cond do
|
||||
not spw_valid ->
|
||||
Logger.warning("Delete character: invalid second password")
|
||||
12 # Wrong Password
|
||||
|
||||
not character_belongs_to_account?(character_id, state) ->
|
||||
Logger.warning("Delete character: character does not belong to account")
|
||||
1 # General error
|
||||
|
||||
true ->
|
||||
# Attempt to delete character
|
||||
case Context.delete_character(character_id) do
|
||||
:ok ->
|
||||
Logger.info("Character deleted successfully: id=#{character_id}")
|
||||
# Remove from state's character list
|
||||
0 # Success
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to delete character: #{inspect(reason)}")
|
||||
1 # General error
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Validate second password
|
||||
# TODO: Check if character belongs to account
|
||||
# TODO: Delete character from database
|
||||
|
||||
# For now, send success stub
|
||||
response = Packets.get_delete_char_response(character_id, 0)
|
||||
response = Packets.get_delete_char_response(character_id, result)
|
||||
send_packet(state, response)
|
||||
|
||||
# Update state if successful
|
||||
new_state =
|
||||
if result == 0 do
|
||||
new_char_ids = Enum.reject(Map.get(state, :character_ids, []), &(&1 == character_id))
|
||||
Map.put(state, :character_ids, new_char_ids)
|
||||
else
|
||||
state
|
||||
end
|
||||
|
||||
{:ok, state}
|
||||
{:ok, new_state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -400,30 +535,84 @@ defmodule Odinsea.Login.Handler do
|
||||
Initiates migration to the selected channel.
|
||||
|
||||
Packet structure:
|
||||
- byte: set_spw (1 if setting second password)
|
||||
- int: character_id
|
||||
"""
|
||||
def on_select_character(packet, state) do
|
||||
if not Map.get(state, :logged_in, false) do
|
||||
{:disconnect, :not_logged_in}
|
||||
else
|
||||
{set_spw, packet} = In.decode_byte(packet)
|
||||
{character_id, _packet} = In.decode_int(packet)
|
||||
|
||||
Logger.info("Select character: character_id=#{character_id}, channel=#{state.channel}")
|
||||
|
||||
# TODO: Validate character belongs to account
|
||||
# TODO: Load character data
|
||||
# TODO: Register migration token with channel server
|
||||
|
||||
# Send migration command to connect to channel
|
||||
# TODO: Get actual channel IP/port
|
||||
channel_ip = "127.0.0.1"
|
||||
channel_port = 8585 + (state.channel - 1)
|
||||
|
||||
response = Packets.get_server_ip(false, channel_ip, channel_port, character_id)
|
||||
send_packet(state, response)
|
||||
|
||||
new_state = Map.put(state, :character_id, character_id)
|
||||
{:ok, new_state}
|
||||
|
||||
# Validate character belongs to account
|
||||
unless character_belongs_to_account?(character_id, state) do
|
||||
Logger.warning("Select character: character does not belong to account")
|
||||
{:disconnect, :invalid_character}
|
||||
else
|
||||
# Handle setting second password if requested
|
||||
if set_spw > 0 do
|
||||
{new_spw, _} = In.decode_string(packet)
|
||||
|
||||
# Validate second password length
|
||||
if String.length(new_spw) < 6 || String.length(new_spw) > 16 do
|
||||
response = Packets.get_second_pw_error(0x14)
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
else
|
||||
# Update second password
|
||||
Context.update_second_password(state.account_id, new_spw)
|
||||
Logger.info("Second password set for account: #{state.account_name}")
|
||||
|
||||
# Continue with character selection
|
||||
do_character_migration(character_id, state)
|
||||
end
|
||||
else
|
||||
do_character_migration(character_id, state)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_character_migration(character_id, state) do
|
||||
# Load character data
|
||||
case Context.load_character(character_id) do
|
||||
nil ->
|
||||
Logger.error("Failed to load character: id=#{character_id}")
|
||||
{:disconnect, :character_not_found}
|
||||
|
||||
character ->
|
||||
# Register migration token with channel server
|
||||
migration_token = generate_migration_token()
|
||||
|
||||
# Store migration info in Redis/ETS for channel server
|
||||
:ok = register_migration_token(
|
||||
migration_token,
|
||||
character_id,
|
||||
state.account_id,
|
||||
state.channel
|
||||
)
|
||||
|
||||
# Update login state to server transition
|
||||
Context.update_login_state(state.account_id, 1, state.ip)
|
||||
|
||||
# Get channel IP and port
|
||||
{channel_ip, channel_port} = get_channel_endpoint(state.channel)
|
||||
|
||||
Logger.info("Character migration: char=#{character.name} to channel #{state.channel} (#{channel_ip}:#{channel_port})")
|
||||
|
||||
# Send migration command
|
||||
response = Packets.get_server_ip(false, channel_ip, channel_port, character_id)
|
||||
send_packet(state, response)
|
||||
|
||||
new_state =
|
||||
state
|
||||
|> Map.put(:character_id, character_id)
|
||||
|> Map.put(:migration_token, migration_token)
|
||||
|
||||
{:ok, new_state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -436,21 +625,27 @@ defmodule Odinsea.Login.Handler do
|
||||
|
||||
Packet structure:
|
||||
- string: second_password
|
||||
- int: character_id
|
||||
"""
|
||||
def on_check_spw_request(packet, state) do
|
||||
{spw, _packet} = In.decode_string(packet)
|
||||
|
||||
# TODO: Validate second password
|
||||
stored_spw = Map.get(state, :second_password)
|
||||
|
||||
if stored_spw == nil or stored_spw == spw do
|
||||
# Success - continue with operation
|
||||
{:ok, state}
|
||||
{spw, packet} = In.decode_string(packet)
|
||||
{character_id, _packet} = In.decode_int(packet)
|
||||
|
||||
# Validate character belongs to account
|
||||
unless character_belongs_to_account?(character_id, state) do
|
||||
{:disconnect, :invalid_character}
|
||||
else
|
||||
# Failure - send error
|
||||
response = Packets.get_second_pw_error(15) # Incorrect SPW
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
stored_spw = Map.get(state, :second_password)
|
||||
|
||||
if stored_spw == nil or stored_spw == spw do
|
||||
# Success - migrate to channel
|
||||
do_character_migration(character_id, state)
|
||||
else
|
||||
# Failure - send error
|
||||
response = Packets.get_second_pw_error(15) # Incorrect SPW
|
||||
send_packet(state, response)
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -535,4 +730,163 @@ defmodule Odinsea.Login.Handler do
|
||||
Context.forbidden_name?(char_name) or
|
||||
Context.character_name_exists?(char_name)
|
||||
end
|
||||
|
||||
defp character_belongs_to_account?(character_id, state) do
|
||||
char_ids = Map.get(state, :character_ids, [])
|
||||
character_id in char_ids
|
||||
end
|
||||
|
||||
defp validate_second_password(state, provided_spw) do
|
||||
stored_spw = Map.get(state, :second_password)
|
||||
|
||||
# If no second password set, accept any
|
||||
if stored_spw == nil || stored_spw == "" do
|
||||
true
|
||||
else
|
||||
stored_spw == provided_spw
|
||||
end
|
||||
end
|
||||
|
||||
defp kick_existing_session(account_id, username) do
|
||||
# TODO: Implement session kicking via World server or Redis pub/sub
|
||||
# For now, just update login state to force disconnect on next tick
|
||||
Context.update_login_state(account_id, 0)
|
||||
|
||||
# Publish kick message to Redis for other servers
|
||||
Odinsea.Database.Redis.publish("kick_session", %{account_id: account_id, username: username})
|
||||
:ok
|
||||
end
|
||||
|
||||
defp kick_existing_session_by_name(username) do
|
||||
# Find account by name and kick
|
||||
case Context.get_account_by_name(username) do
|
||||
nil -> :ok
|
||||
account -> kick_existing_session(account.id, username)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_default_items(character_id, top, bottom, shoes, weapon, _job_type) do
|
||||
# Add equipped items
|
||||
Context.create_inventory_item(character_id, :equipped, %{
|
||||
item_id: top,
|
||||
position: -5,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
if bottom > 0 do
|
||||
Context.create_inventory_item(character_id, :equipped, %{
|
||||
item_id: bottom,
|
||||
position: -6,
|
||||
quantity: 1
|
||||
})
|
||||
end
|
||||
|
||||
Context.create_inventory_item(character_id, :equipped, %{
|
||||
item_id: shoes,
|
||||
position: -7,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
Context.create_inventory_item(character_id, :equipped, %{
|
||||
item_id: weapon,
|
||||
position: -11,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
# Add starter potions
|
||||
Context.create_inventory_item(character_id, :use, %{
|
||||
item_id: 2000013,
|
||||
position: 0,
|
||||
quantity: 100
|
||||
})
|
||||
|
||||
Context.create_inventory_item(character_id, :use, %{
|
||||
item_id: 2000014,
|
||||
position: 0,
|
||||
quantity: 100
|
||||
})
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp add_job_specific_starters(character_id, job_type) do
|
||||
case job_type do
|
||||
0 -> # Resistance
|
||||
Context.create_inventory_item(character_id, :etc, %{
|
||||
item_id: 4161001,
|
||||
position: 0,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
1 -> # Adventurer
|
||||
Context.create_inventory_item(character_id, :etc, %{
|
||||
item_id: 4161001,
|
||||
position: 0,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
2 -> # Cygnus
|
||||
# Add starter quests
|
||||
Context.set_quest_progress(character_id, 20022, 1, "1")
|
||||
Context.set_quest_progress(character_id, 20010, 1, nil)
|
||||
|
||||
Context.create_inventory_item(character_id, :etc, %{
|
||||
item_id: 4161047,
|
||||
position: 0,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
3 -> # Aran
|
||||
Context.create_inventory_item(character_id, :etc, %{
|
||||
item_id: 4161048,
|
||||
position: 0,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
4 -> # Evan
|
||||
Context.create_inventory_item(character_id, :etc, %{
|
||||
item_id: 4161052,
|
||||
position: 0,
|
||||
quantity: 1
|
||||
})
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp generate_migration_token do
|
||||
# Generate a unique migration token
|
||||
:crypto.strong_rand_bytes(16) |> Base.encode16(case: :lower)
|
||||
end
|
||||
|
||||
defp register_migration_token(token, character_id, account_id, channel) do
|
||||
# Store in Redis with TTL for channel server to pick up
|
||||
Odinsea.Database.Redis.setex(
|
||||
"migration:#{token}",
|
||||
30, # 30 second TTL
|
||||
Jason.encode!(%{
|
||||
character_id: character_id,
|
||||
account_id: account_id,
|
||||
channel: channel,
|
||||
timestamp: System.system_time(:second)
|
||||
})
|
||||
)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp get_channel_endpoint(channel) do
|
||||
# TODO: Get actual channel IP from World server config
|
||||
# For now, return localhost with calculated port
|
||||
ip = Application.get_env(:odinsea, :channel_ip, "127.0.0.1")
|
||||
base_port = Application.get_env(:odinsea, :channel_base_port, 8585)
|
||||
port = base_port + (channel - 1)
|
||||
{ip, port}
|
||||
end
|
||||
|
||||
defp format_timestamp(naive_datetime) do
|
||||
NaiveDateTime.to_string(naive_datetime)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user