defmodule Odinsea.Net.Processor do @moduledoc """ Central packet routing/dispatch system. Ported from Java PacketProcessor.java Routes incoming packets to appropriate handlers based on: - Server type (Login, Channel, Shop) - Packet opcode """ require Logger alias Odinsea.Net.Packet.In alias Odinsea.Net.Opcodes @type server_type :: :login | :channel | :shop @type client_state :: map() # ================================================================================================== # Main Entry Point # ================================================================================================== @doc """ Main packet handler entry point. Routes packets to appropriate server-specific handlers. ## Parameters - `opcode` - The packet opcode (integer) - `packet` - The InPacket struct (already read past opcode) - `client_state` - The client's state map - `server_type` - :login | :channel | :shop ## Returns - `{:ok, new_state}` - Success with updated state - `{:error, reason, state}` - Error with reason - `{:disconnect, reason}` - Client should be disconnected """ def handle(opcode, %In{} = packet, client_state, server_type) do # Pre-process common packets that apply to all server types case preprocess(opcode, packet, client_state) do {:handled, new_state} -> {:ok, new_state} :continue -> # Route to server-specific handler case server_type do :login -> handle_login(opcode, packet, client_state) :shop -> handle_shop(opcode, packet, client_state) :channel -> handle_channel(opcode, packet, client_state) _ -> Logger.warning("Unknown server type: #{inspect(server_type)}") {:ok, client_state} end end rescue e -> Logger.error("Packet processing error: #{inspect(e)}") {:error, :processing_error, client_state} end # ================================================================================================== # Pre-Processing (Common Packets) # ================================================================================================== @doc """ Pre-processes packets that are common across all server types. Returns {:handled, state} if the packet was fully handled. Returns :continue if the packet should be routed to server-specific handlers. """ # Define opcodes as module attributes for use in guards @cp_security_packet Opcodes.cp_security_packet() @cp_alive_ack Opcodes.cp_alive_ack() @cp_client_dump_log Opcodes.cp_client_dump_log() @cp_hardware_info Opcodes.cp_hardware_info() @cp_inject_packet Opcodes.cp_inject_packet() @cp_set_code_page Opcodes.cp_set_code_page() @cp_window_focus Opcodes.cp_window_focus() @cp_exception_log Opcodes.cp_exception_log() defp preprocess(opcode, packet, state) do case opcode do # Security packet - always handled in pre-process @cp_security_packet -> handle_security_packet(packet, state) {:handled, state} # Alive acknowledgement - keep connection alive @cp_alive_ack -> handle_alive_ack(state) {:handled, state} # Client dump log - debugging/crash reports @cp_client_dump_log -> handle_dump_log(packet, state) {:handled, state} # Hardware info - client machine identification @cp_hardware_info -> handle_hardware_info(packet, state) {:handled, state} # Packet injection attempt - security violation @cp_inject_packet -> handle_inject_packet(packet, state) {:disconnect, :packet_injection} # Code page settings @cp_set_code_page -> handle_code_page(packet, state) {:handled, state} # Window focus - anti-cheat detection @cp_window_focus -> handle_window_focus(packet, state) {:handled, state} # Exception log from client @cp_exception_log -> handle_exception_log(packet, state) {:handled, state} # Not a common packet, continue to server-specific handling _ -> :continue end end # Login opcodes as module attributes @cp_client_hello Opcodes.cp_client_hello() @cp_check_password Opcodes.cp_check_password() @cp_world_info_request Opcodes.cp_world_info_request() @cp_select_world Opcodes.cp_select_world() @cp_check_user_limit Opcodes.cp_check_user_limit() @cp_check_duplicated_id Opcodes.cp_check_duplicated_id() @cp_create_new_character Opcodes.cp_create_new_character() @cp_create_ultimate Opcodes.cp_create_ultimate() @cp_delete_character Opcodes.cp_delete_character() @cp_select_character Opcodes.cp_select_character() @cp_check_spw_request Opcodes.cp_check_spw_request() @cp_rsa_key Opcodes.cp_rsa_key() # ================================================================================================== # Login Server Packet Handlers # ================================================================================================== @doc """ Routes packets for the Login server. Delegates to Odinsea.Login.Handler module. """ defp handle_login(opcode, packet, state) do alias Odinsea.Login.Handler case opcode do # Permission request (client hello / initial handshake) @cp_client_hello -> Handler.on_permission_request(packet, state) # Password check (login authentication) @cp_check_password -> Handler.on_check_password(packet, state) # World info request (server list) @cp_world_info_request -> Handler.on_world_info_request(state) # Select world @cp_select_world -> Handler.on_select_world(packet, state) # Check user limit (channel population check) @cp_check_user_limit -> Handler.on_check_user_limit(state) # Check duplicated ID (character name availability) @cp_check_duplicated_id -> Handler.on_check_duplicated_id(packet, state) # Create new character @cp_create_new_character -> Handler.on_create_new_character(packet, state) # Create ultimate (Cygnus Knights) @cp_create_ultimate -> Handler.on_create_ultimate(packet, state) # Delete character @cp_delete_character -> Handler.on_delete_character(packet, state) # Select character (enter game) @cp_select_character -> Handler.on_select_character(packet, state) # Second password check @cp_check_spw_request -> Handler.on_check_spw_request(packet, state) # RSA key request @cp_rsa_key -> Handler.on_rsa_key(packet, state) # Unhandled login packet _ -> Logger.debug("Unhandled login packet: opcode=0x#{Integer.to_string(opcode, 16)}") {:ok, state} end end # Channel opcodes as module attributes @cp_migrate_in Opcodes.cp_migrate_in() @cp_move_player Opcodes.cp_move_player() @cp_general_chat Opcodes.cp_general_chat() @cp_change_map Opcodes.cp_change_map() # ================================================================================================== # Channel Server Packet Handlers # ================================================================================================== @doc """ Routes packets for the Channel server (game world). Delegates to appropriate handler modules. """ defp handle_channel(opcode, packet, state) do # TODO: Implement channel packet routing # Will route to: # - Odinsea.Channel.Handler.Player (movement, attacks, skills) # - Odinsea.Channel.Handler.Inventory (items, equipment) # - Odinsea.Channel.Handler.Mob (monster interactions) # - Odinsea.Channel.Handler.NPC (NPC dialogs, shops) # - Odinsea.Channel.Handler.Chat (chat, party, guild) # - etc. case opcode do # Migrate in from login server @cp_migrate_in -> {character_id, _} = In.decode_int(packet) handle_migrate_in(character_id, state) # Player movement @cp_move_player -> {:ok, state} # TODO: Implement # General chat @cp_general_chat -> {:ok, state} # TODO: Implement # Change map @cp_change_map -> {:ok, state} # TODO: Implement # Unhandled channel packet _ -> Logger.debug("Unhandled channel packet: opcode=0x#{Integer.to_string(opcode, 16)}") {:ok, state} end end # Shop opcodes as module attributes @cp_buy_cs_item Opcodes.cp_buy_cs_item() @cp_coupon_code Opcodes.cp_coupon_code() @cp_cs_update Opcodes.cp_cs_update() # ================================================================================================== # Cash Shop Server Packet Handlers # ================================================================================================== @doc """ Routes packets for the Cash Shop server. Delegates to Odinsea.Shop.Handler module. """ defp handle_shop(opcode, packet, state) do # TODO: Implement cash shop packet routing case opcode do # Migrate in from channel server @cp_migrate_in -> {character_id, _} = In.decode_int(packet) handle_migrate_in(character_id, state) # Buy cash item @cp_buy_cs_item -> {:ok, state} # TODO: Implement # Coupon code @cp_coupon_code -> {:ok, state} # TODO: Implement # Cash shop update @cp_cs_update -> {:ok, state} # TODO: Implement # Leave cash shop @cp_change_map -> {:ok, state} # TODO: Implement # Unhandled shop packet _ -> Logger.debug("Unhandled shop packet: opcode=0x#{Integer.to_string(opcode, 16)}") {:ok, state} end end # ================================================================================================== # Common Packet Implementations # ================================================================================================== defp handle_security_packet(_packet, _state) do # Security packet - just acknowledge # In the Java version, this is a no-op that returns true in preProcess :ok end defp handle_alive_ack(state) do # Update last alive time new_state = Map.put(state, :last_alive, System.system_time(:millisecond)) Logger.debug("Alive ack received from #{state.ip}") {:ok, new_state} end defp handle_dump_log(packet, state) do {call_type, packet} = In.decode_short(packet) {error_code, packet} = In.decode_int(packet) {backup_buffer_size, packet} = In.decode_short(packet) {raw_seq, packet} = In.decode_int(packet) {p_type, packet} = In.decode_short(packet) {backup_buffer, _packet} = In.decode_buffer(packet, backup_buffer_size - 6) log_msg = "[ClientDumpLog] RawSeq: #{raw_seq} CallType: #{call_type} " <> "ErrorCode: #{error_code} BufferSize: #{backup_buffer_size} " <> "Type: 0x#{Integer.to_string(p_type, 16)} " <> "Packet: #{inspect(backup_buffer, limit: :infinity)}" Logger.warning(log_msg) :ok end defp handle_hardware_info(packet, state) do {hardware_info, _packet} = In.decode_string(packet) new_state = Map.put(state, :hardware_info, hardware_info) Logger.debug("Hardware info: #{hardware_info}") {:ok, new_state} end defp handle_inject_packet(packet, state) do start = packet.position finish = byte_size(packet.data) - 2 # Read opcode at end packet = %{packet | position: finish} {opcode, _packet} = In.decode_short(packet) # Get hex string of injected packet injected_data = binary_part(packet.data, start, finish - start) player_name = Map.get(state, :character_name, "") player_id = Map.get(state, :character_id, 0) Logger.error( "[InjectPacket] [Session #{state.ip}] [Player #{player_name} - #{player_id}] " <> "[OpCode 0x#{Integer.to_string(opcode, 16)}] #{inspect(injected_data, limit: :infinity)}" ) :ok end defp handle_code_page(packet, state) do {code_page, packet} = In.decode_int(packet) {code_page_read, _packet} = In.decode_int(packet) new_state = state |> Map.put(:code_page, code_page) |> Map.put(:code_page_read, code_page_read) Logger.debug("Code page set: #{code_page}, read: #{code_page_read}") {:ok, new_state} end defp handle_window_focus(packet, state) do {focus, _packet} = In.decode_byte(packet) if focus == 0 do player_name = Map.get(state, :character_name, "") player_id = Map.get(state, :character_id, 0) Logger.warning( "[WindowFocus] Client lost focus [Session #{state.ip}] [Player #{player_name} - #{player_id}]" ) end :ok end defp handle_exception_log(packet, state) do {exception_msg, _packet} = In.decode_string(packet) Logger.warning("[ClientExceptionLog] [Session #{state.ip}] #{exception_msg}") :ok end defp handle_migrate_in(character_id, state) do # TODO: Load character from database, restore session Logger.info("Migrate in: character_id=#{character_id}") new_state = Map.put(state, :character_id, character_id) {:ok, new_state} end end