511 lines
17 KiB
Elixir
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
|