defmodule Odinsea.Login.Handler do @moduledoc """ Login server packet handlers. Ported from Java CharLoginHandler.java Handles all login-related operations: - Authentication (login/password) - World/server selection - Character list - Character creation/deletion - Character selection (migration to channel) """ require Logger alias Odinsea.Net.Packet.{In, Out} alias Odinsea.Net.Cipher.LoginCrypto alias Odinsea.Login.Packets alias Odinsea.Constants.Server alias Odinsea.Database.Context # ================================================================================================== # Permission Request (Client Hello / Version Check) # ================================================================================================== @doc """ Handles permission request (initial client hello). Validates client version, locale, and patch. Packet structure: - byte: locale - short: major version - short: minor version (patch) """ def on_permission_request(packet, state) do {locale, packet} = In.decode_byte(packet) {major, packet} = In.decode_short(packet) {minor, _packet} = In.decode_short(packet) # Validate version if locale != Server.maple_locale() or major != Server.maple_version() or Integer.to_string(minor) != Server.maple_patch() do Logger.warning("Invalid client version: locale=#{locale}, major=#{major}, minor=#{minor}") {:disconnect, :invalid_version} else Logger.debug("Permission request validated") {:ok, state} end end # ================================================================================================== # Password Check (Authentication) # ================================================================================================== @doc """ Handles password check (login authentication). Packet structure: - string: username - string: password (RSA encrypted if enabled) """ def on_check_password(packet, state) do {username, packet} = In.decode_string(packet) {password_encrypted, _packet} = In.decode_string(packet) # Decrypt password if encryption is enabled password = if Application.get_env(:odinsea, :encrypt_passwords, false) do case LoginCrypto.decrypt_rsa(password_encrypted) do {:ok, decrypted} -> decrypted {:error, _} -> password_encrypted end else password_encrypted end Logger.info("Login attempt: username=#{username} from #{state.ip}") # Check if IP/MAC is banned # TODO: Implement IP/MAC ban checking is_banned = false # 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) # 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) send_packet(state, response) {:ok, new_state} {: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) 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} {: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} end end # ================================================================================================== # World Info Request (Server List) # ================================================================================================== @doc """ Handles world info request. Sends the list of available worlds/servers and channels. """ def on_world_info_request(state) do # TODO: Get actual channel load from World server # For now, send a stub response channel_load = get_channel_load() # Send server list server_list = Packets.get_server_list( 0, # server_id "Odinsea", # world_name 0, # flag (0=normal, 1=event, 2=new, 3=hot) "Welcome to Odinsea!", # event_message channel_load ) send_packet(state, server_list) # Send end of server list end_list = Packets.get_end_of_server_list() send_packet(state, end_list) # Send latest connected world latest_world = Packets.get_latest_connected_world(0) send_packet(state, latest_world) # Send recommended world message recommend = Packets.get_recommend_world_message(0, "Join now!") send_packet(state, recommend) {:ok, state} end # ================================================================================================== # Check User Limit (Server Capacity) # ================================================================================================== @doc """ Handles check user limit request. Returns server population status. """ def on_check_user_limit(state) do # TODO: Get actual user count from World server {users_online, user_limit} = get_user_count() status = cond do users_online >= user_limit -> 2 # Full users_online * 2 >= user_limit -> 1 # Highly populated true -> 0 # Normal end response = Packets.get_server_status(status) send_packet(state, response) {:ok, state} end # ================================================================================================== # Select World (Load Character List) # ================================================================================================== @doc """ Handles world selection. Validates world/channel and sends character list. Packet structure: - byte: world_id - byte: channel_id (0-based, add 1 for actual channel) """ def on_select_world(packet, state) do if not Map.get(state, :logged_in, false) do Logger.warning("Select world: not logged in") {:disconnect, :not_logged_in} else {world_id, packet} = In.decode_byte(packet) {channel_id, _packet} = In.decode_byte(packet) actual_channel = channel_id + 1 # Validate world if world_id != 0 do Logger.warning("Invalid world ID: #{world_id}") response = Packets.get_login_failed(10) send_packet(state, response) {:ok, state} else # TODO: Check if channel is available # if not World.is_channel_available(actual_channel) do # response = Packets.get_login_failed(10) # send_packet(state, response) # {:ok, state} # else # TODO: Load character list from database characters = load_characters(state.account_id, world_id) response = Packets.get_char_list( characters, state.second_password, 3 # character slots ) send_packet(state, response) new_state = state |> Map.put(:world, world_id) |> Map.put(:channel, actual_channel) {:ok, new_state} end end end # ================================================================================================== # Check Duplicated ID (Character Name Availability) # ================================================================================================== @doc """ Handles character name availability check. Packet structure: - string: character_name """ def on_check_duplicated_id(packet, state) do if not Map.get(state, :logged_in, false) do {:disconnect, :not_logged_in} else {char_name, _packet} = In.decode_string(packet) # TODO: 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) send_packet(state, response) {:ok, state} end end # ================================================================================================== # Create New Character # ================================================================================================== @doc """ Handles character creation. Packet structure: - string: name - int: job_type (0=Resistance, 1=Adventurer, 2=Cygnus, 3=Aran, 4=Evan) - short: dual_blade (1=DB, 0=Adventurer) - byte: gender (GMS only) - int: face - int: hair - int: hair_color - int: skin_color - int: top - int: bottom - int: shoes - int: weapon """ def on_create_new_character(packet, state) do if not Map.get(state, :logged_in, false) 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) {gender, packet} = In.decode_byte(packet) {face, packet} = In.decode_int(packet) {hair, packet} = In.decode_int(packet) {hair_color, packet} = In.decode_int(packet) {skin_color, packet} = In.decode_int(packet) {top, packet} = In.decode_int(packet) {bottom, packet} = In.decode_int(packet) {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} end end # ================================================================================================== # Create Ultimate (Cygnus Knight → Ultimate Adventurer) # ================================================================================================== @doc """ Handles ultimate adventurer creation (Cygnus Knight transformation). """ def on_create_ultimate(_packet, state) do # TODO: Implement ultimate creation # Requires level 120 Cygnus Knight in Ereve Logger.debug("Create ultimate request (not implemented)") {:ok, state} end # ================================================================================================== # Delete Character # ================================================================================================== @doc """ Handles character deletion. Packet structure: - string: second_password (if enabled) - 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) {character_id, _packet} = In.decode_int(packet) Logger.info("Delete character: character_id=#{character_id}") # 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) send_packet(state, response) {:ok, state} end end # ================================================================================================== # Select Character (Enter Game / Migrate to Channel) # ================================================================================================== @doc """ Handles character selection (enter game). Initiates migration to the selected channel. Packet structure: - int: character_id """ def on_select_character(packet, state) do if not Map.get(state, :logged_in, false) do {:disconnect, :not_logged_in} else {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} end end # ================================================================================================== # Check Second Password # ================================================================================================== @doc """ Handles second password verification (SPW / PIC). Packet structure: - string: second_password """ 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} else # Failure - send error response = Packets.get_second_pw_error(15) # Incorrect SPW send_packet(state, response) {:ok, state} end end # ================================================================================================== # RSA Key Request # ================================================================================================== @doc """ Handles RSA key request. Sends RSA public key and login background. """ def on_rsa_key(_packet, state) do # TODO: Check if custom client is enabled # TODO: Send damage cap packet if custom client # Send login background background = Application.get_env(:odinsea, :login_background, "MapLogin") bg_response = Packets.get_login_background(background) send_packet(state, bg_response) # Send RSA public key pub_key = Application.get_env(:odinsea, :rsa_public_key, "") key_response = Packets.get_rsa_key(pub_key) send_packet(state, key_response) {:ok, state} end # ================================================================================================== # Helper Functions # ================================================================================================== defp send_packet(%{socket: socket} = state, packet_data) do # Add header (2 bytes: packet length) packet_length = byte_size(packet_data) header = <> full_packet = header <> packet_data case :gen_tcp.send(socket, full_packet) do :ok -> Logger.debug("Sent packet: #{packet_length} bytes") :ok {:error, reason} -> Logger.error("Failed to send packet: #{inspect(reason)}") {:error, reason} end end defp send_packet(_state, _packet_data) do # Socket not available in state Logger.error("Cannot send packet: socket not in state") :error end defp authenticate_user(username, password, _state) do # Delegated to Context.authenticate_user/3 Context.authenticate_user(username, password) end defp get_channel_load do # TODO: Get actual channel load from World server # For now, return stub data %{ 1 => 100, 2 => 200, 3 => 150 } end defp get_user_count do # TODO: Get actual user count from World server {50, 1000} end defp load_characters(account_id, world_id) do Context.load_character_entries(account_id, world_id) end defp check_name_used(char_name, _state) do # Check if name is forbidden or already exists Context.forbidden_name?(char_name) or Context.character_name_exists?(char_name) end end