Files
odinsea-elixir/lib/odinsea/channel/handler/players.ex
2026-02-14 23:12:33 -07:00

675 lines
21 KiB
Elixir

defmodule Odinsea.Channel.Handler.Players do
@moduledoc """
Handles general player operation packets.
Ported from: src/handling/channel/handler/PlayersHandler.java
## Main Handlers
- handle_note/2 - Cash note system
- handle_give_fame/2 - Fame system
- handle_use_door/2 - Party door usage
- handle_use_mech_door/2 - Mechanic door usage
- handle_transform_player/2 - Transformation items
- handle_hit_reactor/2 - Reactor hit
- handle_touch_reactor/2 - Reactor touch
- handle_hit_coconut/2 - Coconut event
- handle_follow_request/2 - Follow request
- handle_follow_reply/2 - Follow reply
- handle_ring_action/2 - Marriage rings
- handle_solomon/2 - Solomon's books
- handle_gach_exp/2 - Gachapon EXP
- handle_report/2 - Player reporting
- handle_monster_book_info/2 - Monster book info
- handle_change_set/2 - Card set change
- handle_enter_pvp/2 - Enter PVP
- handle_respawn_pvp/2 - PVP respawn
- handle_leave_pvp/2 - Leave PVP
- handle_attack_pvp/2 - PVP attack
"""
require Logger
alias Odinsea.Net.Packet.In
alias Odinsea.Game.{Character, Map}
alias Odinsea.Channel.Packets
# ============================================================================
# Note System
# ============================================================================
@doc """
Handles cash note operations (CP_NOTE_ACTION / 0xAD).
Types:
- 0: Send note with item
- 1: Delete notes
Reference: PlayersHandler.Note()
"""
def handle_note(packet, client_pid) do
type = In.decode_byte(packet)
case type do
0 ->
# Send note
name = In.decode_string(packet)
msg = In.decode_string(packet)
fame = In.decode_byte(packet) > 0
_ = In.decode_int(packet) # unknown
cash_id = In.decode_long(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Validate item exists in cash inventory
# TODO: Send note to recipient
Logger.debug("Send note to #{name}: #{msg}, fame: #{fame}, cash_id: #{cash_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to send note: #{inspect(reason)}")
end
1 ->
# Delete notes
num = In.decode_byte(packet)
_ = In.decode_short(packet) # skip 2
notes_to_delete = Enum.map(1..num, fn _ ->
id = In.decode_int(packet)
fame_delete = In.decode_byte(packet) > 0
{id, fame_delete}
end)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Delete notes from database
Logger.debug("Delete notes: #{inspect(notes_to_delete)}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to delete notes: #{inspect(reason)}")
end
_ ->
Logger.warning("Unhandled note action: #{type}")
end
end
# ============================================================================
# Fame System
# ============================================================================
@doc """
Handles giving fame (CP_GIVE_FAME / 0x73).
Reference: PlayersHandler.GiveFame()
"""
def handle_give_fame(packet, client_pid) do
target_id = In.decode_int(packet)
mode = In.decode_byte(packet) # 0 = down, 1 = up
fame_change = if mode == 0, do: -1, else: 1
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate target exists on map
# TODO: Check target is not self
# TODO: Check character level >= 15
# TODO: Check fame cooldown
# TODO: Apply fame change
# TODO: Send response packets
Logger.debug("Give fame: #{fame_change} to #{target_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to give fame: #{inspect(reason)}")
end
end
# ============================================================================
# Door Handlers
# ============================================================================
@doc """
Handles door usage (CP_USE_DOOR / 0xAF).
Mystic Door (Priest skill) - warp to town or back.
Reference: PlayersHandler.UseDoor()
"""
def handle_use_door(packet, client_pid) do
oid = In.decode_int(packet)
mode = In.decode_byte(packet) == 0 # 0 = target to town, 1 = town to target
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Find door by owner ID
# TODO: Validate door is active
# TODO: Warp character to appropriate destination
Logger.debug("Use door: OID #{oid}, mode #{mode}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to use door: #{inspect(reason)}")
end
end
@doc """
Handles mechanic door usage (CP_USE_MECH_DOOR / 0xB0).
Mechanic teleport doors.
Reference: PlayersHandler.UseMechDoor()
"""
def handle_use_mech_door(packet, client_pid) do
oid = In.decode_int(packet)
pos_x = In.decode_short(packet)
pos_y = In.decode_short(packet)
mode = In.decode_byte(packet) # door ID
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# Send enable actions
send(client_pid, {:send_packet, Packets.enable_actions()})
# TODO: Find mechanic door by owner ID and door ID
# TODO: Move character to position
Logger.debug("Use mech door: OID #{oid}, pos (#{pos_x}, #{pos_y}), mode #{mode}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to use mech door: #{inspect(reason)}")
end
end
# ============================================================================
# Transformation
# ============================================================================
@doc """
Handles player transformation (CP_TRANSFORM_PLAYER / 0xD2).
Item-based transformations (e.g., 2212000 - prank item).
Reference: PlayersHandler.TransformPlayer()
"""
def handle_transform_player(packet, client_pid) do
tick = In.decode_int(packet)
slot = In.decode_short(packet)
item_id = In.decode_int(packet)
target_name = In.decode_string(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate item exists in inventory
# TODO: Find target by name
# TODO: Apply transformation effect
# TODO: Consume item
Logger.debug("Transform player: item #{item_id}, slot #{slot}, target #{target_name}, tick #{tick}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to transform player: #{inspect(reason)}")
end
end
# ============================================================================
# Reactor Handlers
# ============================================================================
@doc """
Handles reactor hit (CP_DAMAGE_REACTOR / 0x10F).
Reference: PlayersHandler.HitReactor()
"""
def handle_hit_reactor(packet, client_pid) do
oid = In.decode_int(packet)
char_pos = In.decode_int(packet)
stance = In.decode_short(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Get reactor from map
# TODO: Validate reactor is alive
# TODO: Hit reactor with damage
Logger.debug("Hit reactor: OID #{oid}, char_pos #{char_pos}, stance #{stance}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to hit reactor: #{inspect(reason)}")
end
end
@doc """
Handles reactor touch (CP_TOUCH_REACTOR / 0x110).
Reference: PlayersHandler.TouchReactor()
"""
def handle_touch_reactor(packet, client_pid) do
oid = In.decode_int(packet)
touched = if byte_size(packet.data) == 0, do: true, else: In.decode_byte(packet) > 0
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Get reactor from map
# TODO: Handle touch based on reactor type
# TODO: Check required items for certain reactors
Logger.debug("Touch reactor: OID #{oid}, touched #{touched}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to touch reactor: #{inspect(reason)}")
end
end
# ============================================================================
# Event Handlers
# ============================================================================
@doc """
Handles coconut hit (CP_COCONUT / 0x11B).
Coconut event / Coke Play event.
Reference: PlayersHandler.hitCoconut()
"""
def handle_hit_coconut(packet, client_pid) do
coconut_id = In.decode_short(packet)
# Unknown bytes follow
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Get coconut event for channel
# TODO: Validate coconut can be hit
# TODO: Process hit (falling, bomb, points)
Logger.debug("Hit coconut: ID #{coconut_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to hit coconut: #{inspect(reason)}")
end
end
# ============================================================================
# Follow System
# ============================================================================
@doc """
Handles follow request (CP_FOLLOW_REQUEST / 0x8E).
Reference: PlayersHandler.FollowRequest()
"""
def handle_follow_request(packet, client_pid) do
target_id = In.decode_int(packet)
follow_mode = In.decode_byte(packet) > 0
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Find target on map
# TODO: Check distance
# TODO: Send follow request
Logger.debug("Follow request: target #{target_id}, mode #{follow_mode}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to handle follow request: #{inspect(reason)}")
end
end
@doc """
Handles follow reply (CP_FOLLOW_REPLY / 0x91).
Reference: PlayersHandler.FollowReply()
"""
def handle_follow_reply(packet, client_pid) do
target_id = In.decode_int(packet)
accepted = In.decode_byte(packet) > 0
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate follow request exists
# TODO: Set follow state for both players
# TODO: Broadcast follow effect
Logger.debug("Follow reply: target #{target_id}, accepted #{accepted}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to handle follow reply: #{inspect(reason)}")
end
end
# ============================================================================
# Marriage System
# ============================================================================
@doc """
Handles ring/marriage actions (CP_RING_ACTION / 0xB5).
Modes:
- 0: Propose (DoRing)
- 1: Cancel proposal
- 2: Accept/Deny proposal
- 3: Drop ring (ETC only)
Reference: PlayersHandler.RingAction(), PlayersHandler.DoRing()
"""
def handle_ring_action(packet, client_pid) do
mode = In.decode_byte(packet)
case mode do
0 ->
# Propose
name = In.decode_string(packet)
item_id = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Validate character is not married
# TODO: Validate target exists
# TODO: Validate has ring box item
# TODO: Send proposal
Logger.debug("Marriage proposal to #{name} with item #{item_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to propose: #{inspect(reason)}")
end
1 ->
# Cancel proposal
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Cancel pending proposal
Logger.debug("Cancel marriage proposal, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to cancel proposal: #{inspect(reason)}")
end
2 ->
# Accept/Deny
accepted = In.decode_byte(packet) > 0
name = In.decode_string(packet)
id = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Validate proposal exists
# TODO: If accepted, create rings for both
# TODO: Update marriage IDs
Logger.debug("Marriage reply: #{accepted} to #{name} (#{id}), character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to reply to proposal: #{inspect(reason)}")
end
3 ->
# Drop ring
item_id = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, _char_state} ->
# TODO: Validate ring is ETC type
# TODO: Drop ring from inventory
Logger.debug("Drop ring #{item_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to drop ring: #{inspect(reason)}")
end
_ ->
Logger.warning("Unhandled ring action mode: #{mode}")
end
end
# ============================================================================
# Solomon/Gachapon Systems
# ============================================================================
@doc """
Handles Solomon's books (CP_SOLOMON / 0x8C).
EXP books for level 50 and below.
Reference: PlayersHandler.Solomon()
"""
def handle_solomon(packet, client_pid) do
tick = In.decode_int(packet)
slot = In.decode_short(packet)
item_id = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate character level <= 50
# TODO: Validate has gach exp available
# TODO: Get EXP from item
# TODO: Add gach EXP
# TODO: Remove item
# Send enable actions
send(client_pid, {:send_packet, Packets.enable_actions()})
Logger.debug("Solomon: item #{item_id}, slot #{slot}, tick #{tick}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to use Solomon book: #{inspect(reason)}")
end
end
@doc """
Handles Gachapon EXP claim (CP_GACH_EXP / 0x8D).
Reference: PlayersHandler.GachExp()
"""
def handle_gach_exp(packet, client_pid) do
tick = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Check gach EXP > 0
# TODO: Gain EXP with quest rate
# TODO: Reset gach EXP
# Send enable actions
send(client_pid, {:send_packet, Packets.enable_actions()})
Logger.debug("Gach EXP claim: tick #{tick}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to claim gach EXP: #{inspect(reason)}")
end
end
# ============================================================================
# Reporting
# ============================================================================
@doc """
Handles player report (CP_REPORT / 0x94).
Report types: BOT, HACK, AD, HARASS, etc.
Reference: PlayersHandler.Report()
"""
def handle_report(packet, client_pid) do
# Format varies by server type (GMS/non-GMS)
report_type = In.decode_byte(packet)
target_name = In.decode_string(packet)
# Additional data may follow
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate target exists
# TODO: Check report cooldown (2 hours)
# TODO: Log report
# TODO: Send to Discord if configured
Logger.debug("Report: type #{report_type}, target #{target_name}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to handle report: #{inspect(reason)}")
end
end
# ============================================================================
# Monster Book
# ============================================================================
@doc """
Handles monster book info request (CP_GET_BOOK_INFO / 0x7FFA).
Reference: PlayersHandler.MonsterBookInfoRequest()
"""
def handle_monster_book_info(packet, client_pid) do
_ = In.decode_int(packet) # unknown
target_id = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Find target player
# TODO: Get monster book info
# TODO: Send info packet
# Send enable actions
send(client_pid, {:send_packet, Packets.enable_actions()})
Logger.debug("Monster book info request: target #{target_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to get monster book info: #{inspect(reason)}")
end
end
@doc """
Handles card set change (CP_CHANGE_SET / 0x7FFE).
Reference: PlayersHandler.ChangeSet()
"""
def handle_change_set(packet, client_pid) do
set_id = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate set exists
# TODO: Change active card set
# TODO: Apply book effects
Logger.debug("Change card set: #{set_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to change card set: #{inspect(reason)}")
end
end
# ============================================================================
# PVP Handlers
# ============================================================================
@doc """
Handles enter PVP (CP_ENTER_PVP / 0x26).
Reference: PlayersHandler.EnterPVP()
"""
def handle_enter_pvp(packet, client_pid) do
tick = In.decode_int(packet)
_ = In.decode_byte(packet) # skip
type = In.decode_byte(packet)
level = In.decode_byte(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate not in party
# TODO: Validate level range
# TODO: Get PVP event manager
# TODO: Register player for PVP
Logger.debug("Enter PVP: type #{type}, level #{level}, tick #{tick}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to enter PVP: #{inspect(reason)}")
end
end
@doc """
Handles PVP respawn (CP_PVP_RESPAWN / 0x9D).
Reference: PlayersHandler.RespawnPVP()
"""
def handle_respawn_pvp(packet, client_pid) do
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Check player is dead and in PVP
# TODO: Heal player
# TODO: Clear cooldowns
# TODO: Warp to spawn point
# TODO: Send score packet
Logger.debug("PVP respawn: character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to respawn in PVP: #{inspect(reason)}")
end
end
@doc """
Handles leave PVP (CP_LEAVE_PVP / 0x29).
Reference: PlayersHandler.LeavePVP()
"""
def handle_leave_pvp(packet, client_pid) do
tick = In.decode_int(packet)
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Calculate battle points/EXP
# TODO: Clear buffs
# TODO: Warp to lobby (960000000)
# TODO: Update stats
Logger.debug("Leave PVP: tick #{tick}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to leave PVP: #{inspect(reason)}")
end
end
@doc """
Handles PVP attack (CP_PVP_ATTACK / 0x35).
Reference: PlayersHandler.AttackPVP()
"""
def handle_attack_pvp(packet, client_pid) do
skill_id = In.decode_int(packet)
# Complex packet structure for attack data
case Character.get_state_by_client(client_pid) do
{:ok, character_id, char_state} ->
# TODO: Validate in PVP and alive
# TODO: Parse attack data
# TODO: Calculate damage
# TODO: Apply damage to targets
# TODO: Update score
# TODO: Broadcast attack
Logger.debug("PVP attack: skill #{skill_id}, character #{character_id}")
:ok
{:error, reason} ->
Logger.warn("Failed to handle PVP attack: #{inspect(reason)}")
end
end
end