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