414 lines
13 KiB
Elixir
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
|