Files
odinsea-elixir/lib/odinsea/net/processor.ex

414 lines
13 KiB
Elixir

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, "<none>")
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, "<none>")
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