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

511 lines
17 KiB
Elixir

defmodule Odinsea.Channel.Handler.Party do
@moduledoc """
Handles party operations.
Ported from src/handling/channel/handler/PartyHandler.java
Manages party create, join, leave, expel, and leader change.
"""
require Logger
alias Odinsea.Net.Packet.In
alias Odinsea.Channel.Packets
alias Odinsea.Game.Character
alias Odinsea.World.Party
@party_invite_quest_id 1000 # TODO: Get actual quest ID
@party_request_quest_id 1001 # TODO: Get actual quest ID
@doc """
Handles party operations (CP_PARTY_OPERATION).
Ported from PartyHandler.PartyOperation()
Operation:
- 1: Create party
- 2: Leave party
- 3: Accept invitation
- 4: Invite player
- 5: Expel member
- 6: Change leader
- 7: Request to join party
- 8: Toggle party requests
"""
def handle_party_operation(packet, client_state) do
with {:ok, character_pid} <- get_character(client_state),
{:ok, character} <- Character.get_state(character_pid) do
{operation, packet} = In.decode_byte(packet)
Logger.debug("Party operation: #{operation} from #{character.name}")
case operation do
1 -> handle_create_party(character, client_state)
2 -> handle_leave_party(character, client_state)
3 -> handle_accept_invitation(packet, character, client_state)
4 -> handle_invite_player(packet, character, client_state)
5 -> handle_expel_member(packet, character, client_state)
6 -> handle_change_leader(packet, character, client_state)
7 -> handle_request_join(packet, character, client_state)
8 -> handle_toggle_requests(packet, character, client_state)
_ ->
Logger.warning("Unknown party operation: #{operation}")
{:ok, client_state}
end
else
{:error, reason} ->
Logger.warning("Party operation failed: #{inspect(reason)}")
{:ok, client_state}
end
end
@doc """
Handles party request denial (CP_DENY_PARTY_REQUEST).
Ported from PartyHandler.DenyPartyRequest()
"""
def handle_deny_party_request(packet, client_state) do
with {:ok, _character_pid} <- get_character(client_state),
{:ok, character} <- Character.get_state(client_state.character_id) do
{action, packet} = In.decode_byte(packet)
# Check for GMS-specific action
if action == 0x32 do
# TODO: GMS-specific party join
{:ok, client_state}
else
{party_id, _packet} = In.decode_int(packet)
if action == 0x1D || action == 0x1B do
# Accept - handled by PartyOperation(3)
{:ok, client_state}
else
# Deny - notify inviter
case Party.get_party(party_id) do
nil -> :ok
party ->
# Find leader and notify
notify_party_denied(party.leader_id, character.name)
end
{:ok, client_state}
end
end
else
_ -> {:ok, client_state}
end
end
@doc """
Handles party invite settings (CP_ALLOW_PARTY_INVITE).
Ported from PartyHandler.AllowPartyInvite()
"""
def handle_allow_party_invite(packet, character) do
{enabled, _packet} = In.decode_byte(packet)
# Update quest status for party invite blocking
if enabled > 0 do
Character.remove_quest(character.id, @party_invite_quest_id)
else
Character.start_quest(character.id, @party_invite_quest_id)
end
:ok
end
# ============================================================================
# Party Operation Handlers
# ============================================================================
defp handle_create_party(character, client_state) do
if character.party_id && character.party_id > 0 do
# Already in a party
case Party.get_party(character.party_id) do
nil ->
# Invalid party, create new
create_new_party(character, client_state)
party ->
# Check if leader of single-member party
if party.leader_id == character.id && length(party.members) == 1 do
# Re-send party created
party_created_packet = Packets.party_created(party.id)
send_packet(client_state, party_created_packet)
else
Character.send_message(character.id, "You can't create a party as you are already in one", 5)
end
end
else
create_new_party(character, client_state)
end
{:ok, client_state}
end
defp create_new_party(character, client_state) do
party_character = create_party_character(character, client_state.channel_id)
case Party.create_party(party_character) do
{:ok, party} ->
# Update character's party
Character.set_party(character.id, party.id)
# Send party created packet
party_created_packet = Packets.party_created(party.id)
send_packet(client_state, party_created_packet)
Logger.info("Party #{party.id} created by #{character.name}")
{:error, reason} ->
Logger.error("Failed to create party: #{inspect(reason)}")
Character.send_message(character.id, "Failed to create party", 5)
end
end
defp handle_leave_party(character, client_state) do
if character.party_id && character.party_id > 0 do
party_character = create_party_character(character, client_state.channel_id)
case Party.update_party(character.party_id, :leave, party_character) do
{:ok, _} ->
# Update character
Character.set_party(character.id, nil)
# If in Dojo or Pyramid, fail those
# TODO: Implement Dojo/Pyramid fail
# If in event instance, handle leave
# TODO: Implement event instance leftParty
Logger.info("#{character.name} left party #{character.party_id}")
{:error, reason} ->
Logger.error("Failed to leave party: #{inspect(reason)}")
end
end
{:ok, client_state}
end
defp handle_accept_invitation(packet, character, client_state) do
{party_id, _packet} = In.decode_int(packet)
if character.party_id && character.party_id > 0 do
Character.send_message(character.id, "You can't join the party as you are already in one", 5)
else
# Check if accepting party invites
if Character.has_quest(character.id, @party_invite_quest_id) do
{:ok, client_state}
else
case Party.get_party(party_id) do
nil ->
Character.send_message(character.id, "The party you are trying to join does not exist", 5)
party ->
if length(party.members) >= 6 do
send_party_status_message(client_state, 17)
else
# Join party
party_character = create_party_character(character, client_state.channel_id)
case Party.update_party(party_id, :join, party_character) do
{:ok, _} ->
Character.set_party(character.id, party_id)
# Request party member HP updates
# TODO: Implement receivePartyMemberHP / updatePartyMemberHP
Logger.info("#{character.name} joined party #{party_id}")
{:error, :party_full} ->
send_party_status_message(client_state, 17)
{:error, reason} ->
Logger.error("Failed to join party: #{inspect(reason)}")
end
end
end
end
end
{:ok, client_state}
end
defp handle_invite_player(packet, character, client_state) do
# Create party if not in one
party = if not (character.party_id && character.party_id > 0) do
party_character = create_party_character(character, client_state.channel_id)
{:ok, new_party} = Party.create_party(party_character)
Character.set_party(character.id, new_party.id)
party_created_packet = Packets.party_created(new_party.id)
send_packet(client_state, party_created_packet)
new_party
else
Party.get_party(character.party_id)
end
{target_name, _packet} = In.decode_string(packet)
target_name = String.downcase(target_name)
cond do
party && party.expedition_id > 0 ->
Character.send_message(character.id, "You may not do party operations while in a raid.", 5)
party && length(party.members) >= 6 ->
send_party_status_message(client_state, 16)
true ->
# Find target character
case Odinsea.Channel.Players.find_by_name(client_state.channel_id, target_name) do
{:ok, target} ->
# Check if can invite
if can_invite_to_party?(character, target) do
# Send invite
send_party_invite(target, character)
send_party_status_message(client_state, 22, target.name)
Logger.info("#{character.name} invited #{target.name} to party")
else
send_party_status_message(client_state, 17)
end
{:error, :not_found} ->
send_party_status_message(client_state, 19)
end
end
{:ok, client_state}
end
defp handle_expel_member(packet, character, client_state) do
{target_id, _packet} = In.decode_int(packet)
if character.party_id && character.party_id > 0 do
case Party.get_party(character.party_id) do
nil ->
:ok
party ->
# Check if leader
if party.leader_id == character.id do
# Check expedition
if party.expedition_id > 0 do
Character.send_message(character.id, "You may not do party operations while in a raid.", 5)
else
# Find member to expel
target = Enum.find(party.members, fn m -> m.id == target_id end)
if target do
party_character = %{create_party_character(character, client_state.channel_id) | id: target_id}
case Party.update_party(character.party_id, :expel, party_character) do
{:ok, _} ->
# Update expelled character
Character.set_party(target_id, nil)
# Handle event instance
# TODO: disbandParty if leader wants to boot
Logger.info("#{target.name} expelled from party by #{character.name}")
{:error, reason} ->
Logger.error("Failed to expel member: #{inspect(reason)}")
end
end
end
end
end
end
{:ok, client_state}
end
defp handle_change_leader(packet, character, client_state) do
{new_leader_id, _packet} = In.decode_int(packet)
if character.party_id && character.party_id > 0 do
case Party.get_party(character.party_id) do
nil ->
:ok
party ->
# Check expedition
if party.expedition_id > 0 do
Character.send_message(character.id, "You may not do party operations while in a raid.", 5)
else
# Check if leader
if party.leader_id == character.id do
# Check if new leader is in party
if Enum.any?(party.members, fn m -> m.id == new_leader_id end) do
case Party.change_leader(character.party_id, new_leader_id, character.id) do
:ok ->
Logger.info("Party #{character.party_id} leader changed to #{new_leader_id}")
{:error, reason} ->
Logger.error("Failed to change leader: #{inspect(reason)}")
end
end
end
end
end
end
{:ok, client_state}
end
defp handle_request_join(packet, character, client_state) do
{party_id, _packet} = In.decode_int(packet)
# Leave current party if any
if character.party_id && character.party_id > 0 do
handle_leave_party(character, client_state)
end
# Request to join party
case Party.get_party(party_id) do
nil ->
:ok
party ->
# Check restrictions
# TODO: Check event instance, pyramid, dojo, expedition
# Find leader
case Registry.lookup(Odinsea.CharacterRegistry, party.leader_id) do
[{leader_pid, _}] ->
case Character.get_state(leader_pid) do
{:ok, leader} ->
# Check if leader accepts party requests
unless Character.has_quest(leader.id, @party_request_quest_id) do
# Check blacklist
unless Enum.member?(leader.blacklist, String.downcase(character.name)) do
# Send request to leader
send_party_request(leader, character)
send_party_status_message(client_state, 50, character.name)
else
Character.send_message(character.id, "Player was not found or player is not accepting party requests.", 5)
end
else
Character.send_message(character.id, "Player was not found or player is not accepting party requests.", 5)
end
_ ->
Character.send_message(character.id, "Player was not found or player is not accepting party requests.", 5)
end
[] ->
Character.send_message(character.id, "Player was not found or player is not accepting party requests.", 5)
end
end
{:ok, client_state}
end
defp handle_toggle_requests(packet, character, client_state) do
{enabled, _packet} = In.decode_byte(packet)
if enabled > 0 do
Character.remove_quest(character.id, @party_request_quest_id)
else
Character.start_quest(character.id, @party_request_quest_id)
end
{:ok, client_state}
end
# ============================================================================
# Helper Functions
# ============================================================================
defp get_character(client_state) do
case client_state.character_id do
nil -> {:error, :no_character}
character_id ->
case Registry.lookup(Odinsea.CharacterRegistry, character_id) do
[{pid, _}] -> {:ok, pid}
[] -> {:error, :character_not_found}
end
end
end
defp create_party_character(character, channel_id) do
%{
id: character.id,
name: character.name,
level: character.level,
job: character.job,
channel_id: channel_id,
map_id: character.map_id,
# Door info (for mystic door skill)
door_town: 999999999,
door_target: 999999999,
door_skill: 0,
door_x: 0,
door_y: 0
}
end
defp can_invite_to_party?(inviter, target) do
cond do
# Target has blocked inventory
target.has_blocked_inventory ->
false
# Target already in party
target.party_id && target.party_id > 0 ->
false
# Target has blocked invites
Character.has_quest(target.id, @party_invite_quest_id) ->
false
# Target has inviter blacklisted
Enum.member?(target.blacklist, String.downcase(inviter.name)) ->
false
true ->
true
end
end
defp send_party_invite(target, inviter) do
case Registry.lookup(Odinsea.CharacterRegistry, target.id) do
[{pid, _}] ->
invite_packet = Packets.party_invite(inviter)
send(pid, {:send_packet, invite_packet})
[] -> :ok
end
end
defp send_party_request(leader, requester) do
case Registry.lookup(Odinsea.CharacterRegistry, leader.id) do
[{pid, _}] ->
request_packet = Packets.party_request(requester)
send(pid, {:send_packet, request_packet})
[] -> :ok
end
end
defp notify_party_denied(leader_id, denier_name) do
case Registry.lookup(Odinsea.CharacterRegistry, leader_id) do
[{pid, _}] ->
message_packet = Packets.party_status_message(23, denier_name)
send(pid, {:send_packet, message_packet})
[] -> :ok
end
end
defp send_party_status_message(client_state, code, name \\ "") do
packet = Packets.party_status_message(code, name)
send_packet(client_state, packet)
end
defp send_packet(client_state, packet) do
if client_state.socket do
:gen_tcp.send(client_state.socket, packet)
end
end
end