kimi gone wild
This commit is contained in:
566
lib/odinsea/channel/handler/guild.ex
Normal file
566
lib/odinsea/channel/handler/guild.ex
Normal file
@@ -0,0 +1,566 @@
|
||||
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
|
||||
Reference in New Issue
Block a user