567 lines
18 KiB
Elixir
567 lines
18 KiB
Elixir
defmodule Odinsea.Channel.Handler.Guild do
|
|
@moduledoc """
|
|
Handles guild operations.
|
|
Ported from src/handling/channel/handler/GuildHandler.java
|
|
|
|
Manages guild create, join, leave, ranks, skills, and alliance.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.Net.Packet.In
|
|
alias Odinsea.Channel.Packets
|
|
alias Odinsea.Game.Character
|
|
alias Odinsea.World.Guild
|
|
|
|
# Guild creation location (Henesys Guild Headquarters)
|
|
@guild_creation_map_id 200_000_301
|
|
@guild_create_cost 500_000
|
|
@emblem_change_cost 1_500_000
|
|
|
|
# Invited list: {name => {guild_id, expiration_time}}
|
|
@invited_table :guild_invited
|
|
|
|
@doc """
|
|
Initializes the guild handler ETS table.
|
|
"""
|
|
def init do
|
|
:ets.new(@invited_table, [:set, :public, :named_table])
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Handles guild operations (CP_GUILD_OPERATION).
|
|
Ported from GuildHandler.Guild()
|
|
|
|
Operation:
|
|
- 0x02: Create guild
|
|
- 0x05: Invite player
|
|
- 0x06: Accept invitation
|
|
- 0x07: Leave guild
|
|
- 0x08: Expel member
|
|
- 0x0E: Change rank titles
|
|
- 0x0F: Change member rank
|
|
- 0x10: Change emblem
|
|
- 0x11: Change notice
|
|
- 0x1D: Purchase skill
|
|
- 0x1E: Activate skill
|
|
- 0x1F: Change leader
|
|
"""
|
|
def handle_guild_operation(packet, client_state) do
|
|
with {:ok, character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(character_pid) do
|
|
|
|
# Prune expired invites periodically
|
|
prune_expired_invites()
|
|
|
|
{operation, packet} = In.decode_byte(packet)
|
|
|
|
Logger.debug("Guild operation: #{operation} from #{character.name}")
|
|
|
|
case operation do
|
|
0x02 -> handle_create_guild(packet, character, client_state)
|
|
0x05 -> handle_invite_player(packet, character, client_state)
|
|
0x06 -> handle_accept_invitation(packet, character, client_state)
|
|
0x07 -> handle_leave_guild(character, client_state)
|
|
0x08 -> handle_expel_member(packet, character, client_state)
|
|
0x0E -> handle_change_rank_titles(packet, character, client_state)
|
|
0x0F -> handle_change_rank(packet, character, client_state)
|
|
0x10 -> handle_change_emblem(packet, character, client_state)
|
|
0x11 -> handle_change_notice(packet, character, client_state)
|
|
0x1D -> handle_purchase_skill(packet, character, client_state)
|
|
0x1E -> handle_activate_skill(packet, character, client_state)
|
|
0x1F -> handle_change_leader(packet, character, client_state)
|
|
_ ->
|
|
Logger.warning("Unknown guild operation: #{operation}")
|
|
{:ok, client_state}
|
|
end
|
|
else
|
|
{:error, reason} ->
|
|
Logger.warning("Guild operation failed: #{inspect(reason)}")
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Handles guild request denial (CP_DENY_GUILD_REQUEST).
|
|
Ported from GuildHandler.DenyGuildRequest()
|
|
"""
|
|
def handle_deny_guild_request(packet, client_state) do
|
|
with {:ok, _character_pid} <- get_character(client_state),
|
|
{:ok, character} <- Character.get_state(client_state.character_id) do
|
|
|
|
{from_name, _packet} = In.decode_string(packet)
|
|
from_name = String.downcase(from_name)
|
|
|
|
# Remove from invited list
|
|
case :ets.lookup(@invited_table, from_name) do
|
|
[{^from_name, {guild_id, _expires}}] ->
|
|
:ets.delete(@invited_table, from_name)
|
|
|
|
# Notify inviter
|
|
notify_guild_denied(from_name, character.name)
|
|
|
|
[] ->
|
|
:ok
|
|
end
|
|
else
|
|
_ -> :ok
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
# ============================================================================
|
|
# Guild Operation Handlers
|
|
# ============================================================================
|
|
|
|
defp handle_create_guild(packet, character, client_state) do
|
|
cond do
|
|
character.guild_id && character.guild_id > 0 ->
|
|
Character.send_message(character.id, "You cannot create a new Guild while in one.", 1)
|
|
|
|
character.map_id != @guild_creation_map_id ->
|
|
Character.send_message(character.id, "You cannot create a new Guild while in one.", 1)
|
|
|
|
character.meso < @guild_create_cost ->
|
|
Character.send_message(character.id, "You do not have enough mesos to create a Guild.", 1)
|
|
|
|
true ->
|
|
{guild_name, _packet} = In.decode_string(packet)
|
|
|
|
if valid_guild_name?(guild_name) do
|
|
case Guild.create_guild(character.id, guild_name) do
|
|
{:ok, guild_id} ->
|
|
# Deduct mesos
|
|
Character.gain_meso(character.id, -@guild_create_cost, true, true)
|
|
|
|
# Set guild info
|
|
Character.set_guild(character.id, guild_id, 1)
|
|
Character.save_guild_status(character.id)
|
|
|
|
# TODO: Finish achievement 35
|
|
|
|
# Set online in guild
|
|
Guild.set_online(guild_id, character.id, true, client_state.channel_id)
|
|
|
|
# Send guild info
|
|
# TODO: Implement showGuildInfo packet
|
|
|
|
# Gain GP for creation
|
|
Guild.gain_gp(guild_id, 500, character.id)
|
|
|
|
# Respawn player (update guild tag)
|
|
respawn_player(character.id)
|
|
|
|
Character.send_message(character.id, "You have successfully created a Guild.", 1)
|
|
|
|
Logger.info("Guild '#{guild_name}' (ID: #{guild_id}) created by #{character.name}")
|
|
|
|
{:error, reason} ->
|
|
Character.send_message(character.id, "Please try again.", 1)
|
|
Logger.error("Failed to create guild: #{inspect(reason)}")
|
|
end
|
|
else
|
|
Character.send_message(character.id, "The Guild name you have chosen is not accepted.", 1)
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_invite_player(packet, character, client_state) do
|
|
# Check if in guild and has invite permission (rank <= 2)
|
|
if character.guild_id && character.guild_id > 0 && character.guild_rank <= 2 do
|
|
{target_name, _packet} = In.decode_string(packet)
|
|
target_name_lower = String.downcase(target_name)
|
|
|
|
# Check if already handling invitation
|
|
case :ets.lookup(@invited_table, target_name_lower) do
|
|
[{_, _}] ->
|
|
Character.send_message(character.id, "The player is currently handling an invitation.", 5)
|
|
|
|
[] ->
|
|
# Try to find target
|
|
case Odinsea.Channel.Players.find_by_name(client_state.channel_id, target_name_lower) do
|
|
{:ok, target} ->
|
|
# Check if can invite
|
|
if target.guild_id == nil || target.guild_id == 0 do
|
|
# Send invite
|
|
send_guild_invite(target, character)
|
|
|
|
# Add to invited list (expires in 20 minutes)
|
|
expiration = System.system_time(:millisecond) + 20 * 60 * 1000
|
|
:ets.insert(@invited_table, {target_name_lower, {character.guild_id, expiration}})
|
|
else
|
|
# TODO: Send appropriate error packet
|
|
:ok
|
|
end
|
|
|
|
{:error, :not_found} ->
|
|
# TODO: Send error packet
|
|
:ok
|
|
end
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_accept_invitation(packet, character, client_state) do
|
|
if character.guild_id && character.guild_id > 0 do
|
|
# Already in guild
|
|
{:ok, client_state}
|
|
else
|
|
{guild_id, packet} = In.decode_int(packet)
|
|
{cid, _packet} = In.decode_int(packet)
|
|
|
|
# Verify character ID matches
|
|
if cid == character.id do
|
|
target_name = String.downcase(character.name)
|
|
|
|
case :ets.lookup(@invited_table, target_name) do
|
|
[{^target_name, {^guild_id, _expires}}] ->
|
|
# Remove from invited
|
|
:ets.delete(@invited_table, target_name)
|
|
|
|
# Join guild
|
|
case Guild.add_member(guild_id, character) do
|
|
{:ok, _member} ->
|
|
# Set guild info
|
|
Character.set_guild(character.id, guild_id, 5)
|
|
Character.save_guild_status(character.id)
|
|
|
|
# Send guild info
|
|
# TODO: Implement showGuildInfo packet
|
|
|
|
# Send alliance info if applicable
|
|
guild = Guild.get_guild(guild_id)
|
|
if guild && guild.alliance_id > 0 do
|
|
# TODO: Send alliance info
|
|
:ok
|
|
end
|
|
|
|
# Respawn player
|
|
respawn_player(character.id)
|
|
|
|
{:error, :guild_full} ->
|
|
Character.send_message(character.id, "The Guild you are trying to join is already full.", 1)
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to add guild member: #{inspect(reason)}")
|
|
end
|
|
|
|
[] ->
|
|
# No pending invitation
|
|
:ok
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
defp handle_leave_guild(character, client_state) do
|
|
if character.guild_id && character.guild_id > 0 do
|
|
case Guild.leave_guild(character.guild_id, character.id) do
|
|
:ok ->
|
|
# Clear guild info
|
|
Character.set_guild(character.id, 0, 5)
|
|
Character.save_guild_status(character.id)
|
|
|
|
# Send empty guild info
|
|
# TODO: Implement showGuildInfo with null
|
|
|
|
Logger.info("#{character.name} left guild #{character.guild_id}")
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to leave guild: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_expel_member(packet, character, client_state) do
|
|
{target_id, packet} = In.decode_int(packet)
|
|
{target_name, _packet} = In.decode_string(packet)
|
|
|
|
if character.guild_id && character.guild_id > 0 && character.guild_rank <= 2 do
|
|
case Guild.expel_member(character.guild_id, character.id, target_id, target_name) do
|
|
:ok ->
|
|
# Update expelled character if online
|
|
case Registry.lookup(Odinsea.CharacterRegistry, target_id) do
|
|
[{pid, _}] ->
|
|
Character.set_guild(target_id, 0, 5)
|
|
# TODO: Send guild info update
|
|
[] ->
|
|
# Send note to offline character
|
|
send_note(target_name, character.name, "You have been expelled from the guild.")
|
|
end
|
|
|
|
Logger.info("#{target_name} expelled from guild by #{character.name}")
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to expel member: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_change_rank_titles(packet, character, client_state) do
|
|
if character.guild_id && character.guild_id > 0 && character.guild_rank == 1 do
|
|
# Read 5 rank titles
|
|
titles = for _i <- 1..5 do
|
|
{title, remaining} = In.decode_string(packet)
|
|
packet = remaining
|
|
title
|
|
end
|
|
|
|
case Guild.change_rank_titles(character.guild_id, titles, character.id) do
|
|
:ok ->
|
|
Logger.info("Guild #{character.guild_id} rank titles changed")
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to change rank titles: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_change_rank(packet, character, client_state) do
|
|
{target_id, packet} = In.decode_byte(packet)
|
|
{new_rank, _packet} = In.decode_byte(packet)
|
|
|
|
if character.guild_id && character.guild_id > 0 && character.guild_rank <= 2 do
|
|
# Validate rank
|
|
if new_rank > 1 && new_rank <= 5 && (new_rank > 2 || character.guild_rank == 1) do
|
|
case Guild.change_rank(character.guild_id, target_id, new_rank, character.id) do
|
|
:ok ->
|
|
# Update target's rank if online
|
|
case Registry.lookup(Odinsea.CharacterRegistry, target_id) do
|
|
[{pid, _}] ->
|
|
Character.set_guild_rank(target_id, new_rank)
|
|
[] ->
|
|
:ok
|
|
end
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to change rank: #{inspect(reason)}")
|
|
end
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_change_emblem(packet, character, client_state) do
|
|
cond do
|
|
character.guild_id == nil || character.guild_id == 0 ->
|
|
{:ok, client_state}
|
|
|
|
character.guild_rank != 1 ->
|
|
{:ok, client_state}
|
|
|
|
character.map_id != @guild_creation_map_id ->
|
|
{:ok, client_state}
|
|
|
|
character.meso < @emblem_change_cost ->
|
|
Character.send_message(character.id, "You do not have enough mesos to create an emblem.", 1)
|
|
{:ok, client_state}
|
|
|
|
true ->
|
|
{bg, packet} = In.decode_short(packet)
|
|
{bg_color, packet} = In.decode_byte(packet)
|
|
{logo, packet} = In.decode_short(packet)
|
|
{logo_color, _packet} = In.decode_byte(packet)
|
|
|
|
case Guild.set_emblem(character.guild_id, bg, bg_color, logo, logo_color, character.id) do
|
|
:ok ->
|
|
# Deduct mesos
|
|
Character.gain_meso(character.id, -@emblem_change_cost, true, true)
|
|
|
|
# Respawn all members to update emblem
|
|
respawn_all_guild_members(character.guild_id)
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to change emblem: #{inspect(reason)}")
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
end
|
|
|
|
defp handle_change_notice(packet, character, client_state) do
|
|
{notice, _packet} = In.decode_string(packet)
|
|
|
|
if character.guild_id && character.guild_id > 0 && character.guild_rank <= 2 do
|
|
if String.length(notice) <= 100 do
|
|
case Guild.set_notice(character.guild_id, notice, character.id) do
|
|
:ok ->
|
|
Logger.info("Guild #{character.guild_id} notice changed")
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to change notice: #{inspect(reason)}")
|
|
end
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_purchase_skill(packet, character, client_state) do
|
|
{skill_id, _packet} = In.decode_int(packet)
|
|
|
|
if character.guild_id && character.guild_id > 0 do
|
|
# TODO: Validate skill and level
|
|
# TODO: Check if character has enough mesos
|
|
|
|
case Guild.purchase_skill(character.guild_id, skill_id, character.name, character.id) do
|
|
{:ok, _level} ->
|
|
# Deduct mesos
|
|
# TODO: Get skill price
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to purchase guild skill: #{inspect(reason)}")
|
|
end
|
|
end
|
|
|
|
{:ok, client_state}
|
|
end
|
|
|
|
defp handle_activate_skill(packet, character, client_state) do
|
|
{skill_id, _packet} = In.decode_int(packet)
|
|
|
|
if character.guild_id && character.guild_id > 0 do
|
|
# TODO: Check if skill is purchased and not expired
|
|
# TODO: Check if character has enough mesos for extension
|
|
|
|
case Guild.activate_skill(character.guild_id, skill_id, character.name) do
|
|
:ok ->
|
|
# Deduct mesos
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to activate guild skill: #{inspect(reason)}")
|
|
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.guild_id && character.guild_id > 0 && character.guild_rank == 1 do
|
|
# Get current leader
|
|
guild = Guild.get_guild(character.guild_id)
|
|
|
|
if guild && guild.leader_id != new_leader_id do
|
|
case Guild.change_leader(character.guild_id, new_leader_id, character.id) do
|
|
:ok ->
|
|
# Update ranks
|
|
Character.set_guild_rank(character.id, 2)
|
|
Character.set_guild_rank(new_leader_id, 1)
|
|
|
|
{:error, reason} ->
|
|
Character.send_message(character.id, "This user is already the guild leader.", 1)
|
|
Logger.error("Failed to change leader: #{inspect(reason)}")
|
|
end
|
|
else
|
|
Character.send_message(character.id, "This user is already the guild leader.", 1)
|
|
end
|
|
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 valid_guild_name?(name) do
|
|
cond do
|
|
String.length(name) < 3 -> false
|
|
String.length(name) > 12 -> false
|
|
true -> Regex.match?(~r/^[a-zA-Z]+$/, name)
|
|
end
|
|
end
|
|
|
|
defp send_guild_invite(target, inviter) do
|
|
case Registry.lookup(Odinsea.CharacterRegistry, target.id) do
|
|
[{pid, _}] ->
|
|
invite_packet = Packets.guild_invite(inviter)
|
|
send(pid, {:send_packet, invite_packet})
|
|
[] -> :ok
|
|
end
|
|
end
|
|
|
|
defp notify_guild_denied(inviter_name, denier_name) do
|
|
# Find inviter and send denial
|
|
case Odinsea.Channel.Players.find_by_name(1, inviter_name) do
|
|
{:ok, inviter} ->
|
|
case Registry.lookup(Odinsea.CharacterRegistry, inviter.id) do
|
|
[{pid, _}] ->
|
|
packet = Packets.deny_guild_invitation(denier_name)
|
|
send(pid, {:send_packet, packet})
|
|
[] -> :ok
|
|
end
|
|
{:error, _} -> :ok
|
|
end
|
|
end
|
|
|
|
defp send_note(to_name, from_name, message) do
|
|
# TODO: Implement note sending via database
|
|
Logger.debug("Note to #{to_name} from #{from_name}: #{message}")
|
|
:ok
|
|
end
|
|
|
|
defp respawn_player(character_id) do
|
|
case Registry.lookup(Odinsea.CharacterRegistry, character_id) do
|
|
[{pid, _}] ->
|
|
case Character.get_state(pid) do
|
|
{:ok, character} ->
|
|
# Broadcast guild name and icon update
|
|
# TODO: Implement proper respawn
|
|
:ok
|
|
_ -> :ok
|
|
end
|
|
[] -> :ok
|
|
end
|
|
end
|
|
|
|
defp respawn_all_guild_members(guild_id) do
|
|
case Guild.get_guild(guild_id) do
|
|
nil -> :ok
|
|
guild ->
|
|
Enum.each(guild.members, fn member ->
|
|
if member.online do
|
|
respawn_player(member.id)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
defp prune_expired_invites do
|
|
now = System.system_time(:millisecond)
|
|
|
|
:ets.select_delete(@invited_table, [
|
|
{{:_, {:_, :"$1"}}, [{:<, :"$1", now}], [true]}
|
|
])
|
|
end
|
|
end
|