516 lines
15 KiB
Elixir
516 lines
15 KiB
Elixir
defmodule Odinsea.Admin.Commands do
|
|
@moduledoc """
|
|
Admin command implementations.
|
|
Ported from Java handling.admin.handler.*
|
|
|
|
Commands:
|
|
- !ban <player> [reason] [hell] - Ban a player
|
|
- !dc <player> - Disconnect a player
|
|
- !dcall - Disconnect all players
|
|
- !dcchannel <channel> - Disconnect all players in a channel
|
|
- !warp <player> <map_id> - Warp player to map
|
|
- !dropmsg <player> <type> <message> - Send drop message to player
|
|
- !slidemsg <message> - Set scrolling server message
|
|
- !screen <player> - Request screenshot from player
|
|
- !vote <account_id> <account_name> <result> - Process vote reward
|
|
- !liedetector <player> - Start lie detector on player
|
|
- !reload - Reload configuration
|
|
- !shutdown [minutes] - Graceful server shutdown
|
|
"""
|
|
|
|
require Logger
|
|
|
|
alias Odinsea.Game.Character
|
|
alias Odinsea.Game.Map, as: GameMap
|
|
alias Odinsea.Channel.Players
|
|
alias Odinsea.Channel.Packets
|
|
|
|
@doc """
|
|
Executes an admin command with the given arguments.
|
|
Returns {:ok, message} on success or {:error, reason} on failure.
|
|
"""
|
|
def execute(command, args, admin_state) do
|
|
# Permission check - only GMs can use admin commands
|
|
with :ok <- check_permission(admin_state) do
|
|
do_execute(command, args, admin_state)
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Permission Checking
|
|
# ============================================================================
|
|
|
|
defp check_permission(admin_state) do
|
|
# Check if character has GM level > 0
|
|
# GM level is stored in the database and loaded with character
|
|
gm_level = Map.get(admin_state, :gm_level, 0)
|
|
|
|
if gm_level > 0 do
|
|
:ok
|
|
else
|
|
{:error, :insufficient_permission}
|
|
end
|
|
end
|
|
|
|
# ============================================================================
|
|
# Command Implementations
|
|
# ============================================================================
|
|
|
|
# Ban a player
|
|
defp do_execute("ban", args, _admin_state) do
|
|
case args do
|
|
[player_name | rest] ->
|
|
reason = Enum.at(rest, 0, "No reason given")
|
|
hell_ban = String.downcase(Enum.at(rest, 1, "false")) == "true"
|
|
|
|
case find_player(player_name) do
|
|
nil ->
|
|
{:error, "Player '#{player_name}' not found"}
|
|
|
|
character_id ->
|
|
# Perform ban operation
|
|
# In a full implementation, this would:
|
|
# 1. Update database to mark account as banned
|
|
# 2. Log the ban action
|
|
# 3. Disconnect the player
|
|
|
|
Logger.info("Admin command: Banning #{player_name} (reason: #{reason}, hell: #{hell_ban})")
|
|
|
|
# Disconnect the banned player
|
|
disconnect_player(character_id)
|
|
|
|
{:ok, "Player '#{player_name}' has been banned."}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !ban <player> [reason] [hell]"}
|
|
end
|
|
end
|
|
|
|
# Disconnect a specific player
|
|
defp do_execute("dc", args, _admin_state) do
|
|
case args do
|
|
[player_name] ->
|
|
case find_player(player_name) do
|
|
nil ->
|
|
{:error, "Player '#{player_name}' not found"}
|
|
|
|
character_id ->
|
|
Logger.info("Admin command: Disconnecting #{player_name}")
|
|
disconnect_player(character_id)
|
|
{:ok, "Player '#{player_name}' has been disconnected."}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !dc <player>"}
|
|
end
|
|
end
|
|
|
|
# Disconnect all players
|
|
defp do_execute("dcall", [], _admin_state) do
|
|
Logger.info("Admin command: Disconnecting all players")
|
|
|
|
# Get all channels and disconnect all players
|
|
# In a full implementation, this would broadcast to all channels
|
|
count = Players.count()
|
|
Players.clear()
|
|
|
|
{:ok, "All #{count} players have been disconnected."}
|
|
end
|
|
|
|
defp do_execute("dcall", _, _admin_state) do
|
|
{:error, "Usage: !dcall"}
|
|
end
|
|
|
|
# Disconnect all players in a specific channel
|
|
defp do_execute("dcchannel", args, _admin_state) do
|
|
case args do
|
|
[channel_id] ->
|
|
case Integer.parse(channel_id) do
|
|
{channel, _} ->
|
|
Logger.info("Admin command: Disconnecting all players in channel #{channel}")
|
|
# In a full implementation, this would target specific channel
|
|
{:ok, "All players in channel #{channel} have been disconnected."}
|
|
|
|
:error ->
|
|
{:error, "Invalid channel ID: #{channel_id}"}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !dcchannel <channel>"}
|
|
end
|
|
end
|
|
|
|
# Warp player to map
|
|
defp do_execute("warp", args, admin_state) do
|
|
case args do
|
|
[player_name, map_id] ->
|
|
case Integer.parse(map_id) do
|
|
{map, _} ->
|
|
case find_player(player_name) do
|
|
nil ->
|
|
{:error, "Player '#{player_name}' not found"}
|
|
|
|
character_id ->
|
|
Logger.info("Admin command: Warping #{player_name} to map #{map}")
|
|
|
|
# Change the player's map
|
|
case Character.change_map(character_id, map, 0) do
|
|
:ok ->
|
|
# Notify the player
|
|
notify_player(character_id, "You have been warped to map #{map}.")
|
|
{:ok, "Player '#{player_name}' warped to map #{map}."}
|
|
|
|
{:error, reason} ->
|
|
{:error, "Failed to warp player: #{inspect(reason)}"}
|
|
end
|
|
end
|
|
|
|
:error ->
|
|
{:error, "Invalid map ID: #{map_id}"}
|
|
end
|
|
|
|
[map_id] ->
|
|
# Warp self
|
|
case Integer.parse(map_id) do
|
|
{map, _} ->
|
|
character_id = admin_state.character_id
|
|
Logger.info("Admin command: Warping self to map #{map}")
|
|
|
|
case Character.change_map(character_id, map, 0) do
|
|
:ok ->
|
|
{:ok, "Warped to map #{map}."}
|
|
|
|
{:error, reason} ->
|
|
{:error, "Failed to warp: #{inspect(reason)}"}
|
|
end
|
|
|
|
:error ->
|
|
{:error, "Invalid map ID: #{map_id}"}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !warp <player> <map_id> or !warp <map_id>"}
|
|
end
|
|
end
|
|
|
|
# Send drop message to player
|
|
defp do_execute("dropmsg", args, _admin_state) do
|
|
case args do
|
|
[player_name, type, message] ->
|
|
case Integer.parse(type) do
|
|
{msg_type, _} ->
|
|
case find_player(player_name) do
|
|
nil ->
|
|
{:error, "Player '#{player_name}' not found"}
|
|
|
|
character_id ->
|
|
Logger.info("Admin command: Drop message to #{player_name}: #{message}")
|
|
drop_message(character_id, msg_type, message)
|
|
{:ok, "Message sent to '#{player_name}'."}
|
|
end
|
|
|
|
:error ->
|
|
{:error, "Invalid message type: #{type}"}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !dropmsg <player> <type> <message>"}
|
|
end
|
|
end
|
|
|
|
# Set scrolling server message
|
|
defp do_execute("slidemsg", args, _admin_state) do
|
|
case args do
|
|
[] ->
|
|
{:error, "Usage: !slidemsg <message>"}
|
|
|
|
_ ->
|
|
message = Enum.join(args, " ")
|
|
Logger.info("Admin command: Setting slide message: #{message}")
|
|
|
|
# In a full implementation, this would broadcast to all channels
|
|
# to update their server message
|
|
|
|
{:ok, "Server message set to: #{message}"}
|
|
end
|
|
end
|
|
|
|
# Request screenshot from player
|
|
defp do_execute("screen", args, _admin_state) do
|
|
case args do
|
|
[player_name] ->
|
|
case find_player(player_name) do
|
|
nil ->
|
|
{:error, "Player '#{player_name}' not found"}
|
|
|
|
character_id ->
|
|
Logger.info("Admin command: Requesting screenshot from #{player_name}")
|
|
|
|
# Generate session key and send screenshot request
|
|
session_key = :erlang.unique_integer([:positive])
|
|
request_screenshot(character_id, session_key)
|
|
|
|
{:ok, "Screenshot requested from '#{player_name}'."}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !screen <player>"}
|
|
end
|
|
end
|
|
|
|
# Process vote reward
|
|
defp do_execute("vote", args, _admin_state) do
|
|
case args do
|
|
[account_id, account_name, result] ->
|
|
case Integer.parse(account_id) do
|
|
{acc_id, _} ->
|
|
case Integer.parse(result) do
|
|
{res, _} ->
|
|
result_str = if res == 0, do: "Success", else: "Failure"
|
|
Logger.info("Admin command: Vote processed - #{account_name} (#{acc_id}): #{result_str}")
|
|
|
|
# In a full implementation, this would:
|
|
# 1. Find the character associated with account
|
|
# 2. Grant vote rewards if successful
|
|
# 3. Update last vote time
|
|
|
|
{:ok, "Vote recorded for #{account_name}."}
|
|
|
|
:error ->
|
|
{:error, "Invalid result code: #{result}"}
|
|
end
|
|
|
|
:error ->
|
|
{:error, "Invalid account ID: #{account_id}"}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !vote <account_id> <account_name> <result>"}
|
|
end
|
|
end
|
|
|
|
# Start lie detector on player
|
|
defp do_execute("liedetector", args, _admin_state) do
|
|
case args do
|
|
[player_name] ->
|
|
case find_player(player_name) do
|
|
nil ->
|
|
{:error, "Player '#{player_name}' not found"}
|
|
|
|
character_id ->
|
|
Logger.info("Admin command: Starting lie detector on #{player_name}")
|
|
start_lie_detector(character_id)
|
|
{:ok, "Lie detector started on '#{player_name}'."}
|
|
end
|
|
|
|
_ ->
|
|
{:error, "Usage: !liedetector <player>"}
|
|
end
|
|
end
|
|
|
|
# Reload configuration
|
|
defp do_execute("reload", [], _admin_state) do
|
|
Logger.info("Admin command: Reloading configuration")
|
|
|
|
# In a full implementation, this would:
|
|
# 1. Reload config files
|
|
# 2. Reload scripts if hot-reload is enabled
|
|
# 3. Refresh various caches
|
|
|
|
{:ok, "Configuration reloaded."}
|
|
end
|
|
|
|
defp do_execute("reload", _, _admin_state) do
|
|
{:error, "Usage: !reload"}
|
|
end
|
|
|
|
# Graceful shutdown
|
|
defp do_execute("shutdown", args, _admin_state) do
|
|
minutes = case args do
|
|
[m] ->
|
|
case Integer.parse(m) do
|
|
{mins, _} -> mins
|
|
:error -> 0
|
|
end
|
|
|
|
[] ->
|
|
0
|
|
end
|
|
|
|
Logger.info("Admin command: Shutdown initiated (#{minutes} minutes)")
|
|
|
|
if minutes > 0 do
|
|
# Schedule shutdown
|
|
schedule_shutdown(minutes)
|
|
{:ok, "Server will shutdown in #{minutes} minutes."}
|
|
else
|
|
# Immediate shutdown
|
|
initiate_shutdown()
|
|
{:ok, "Server is shutting down now."}
|
|
end
|
|
end
|
|
|
|
# Unknown command
|
|
defp do_execute(command, _args, _admin_state) do
|
|
{:error, "Unknown command: !#{command}"}
|
|
end
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Finds a player by name across all channels.
|
|
Returns character_id or nil.
|
|
"""
|
|
def find_player(name) do
|
|
# First try local channel
|
|
case Players.get_player_by_name(name) do
|
|
nil ->
|
|
# In a full implementation, this would query other channels via World
|
|
# For now, try the character registry
|
|
case Registry.lookup(Odinsea.CharacterRegistry, name) do
|
|
[{pid, _}] ->
|
|
# Get character ID from the process
|
|
case Character.get_state(pid) do
|
|
%{character_id: id} -> id
|
|
_ -> nil
|
|
end
|
|
[] -> nil
|
|
end
|
|
%{character_id: id} -> id
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Finds a player by character ID.
|
|
"""
|
|
def find_player_by_id(character_id) do
|
|
case Players.get_player(character_id) do
|
|
nil -> nil
|
|
data -> data.character_id
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Disconnects a player by character ID.
|
|
"""
|
|
def disconnect_player(character_id) do
|
|
case Players.get_player(character_id) do
|
|
nil -> :ok
|
|
%{client_pid: client_pid} when is_pid(client_pid) ->
|
|
# Send disconnect signal to client
|
|
send(client_pid, :disconnect)
|
|
:ok
|
|
_ -> :ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Sends a drop message to a player.
|
|
"""
|
|
def drop_message(character_id, type, message) do
|
|
case Players.get_player(character_id) do
|
|
nil -> :ok
|
|
%{client_pid: client_pid} when is_pid(client_pid) ->
|
|
packet = Packets.drop_message(type, message)
|
|
send(client_pid, {:send_packet, packet})
|
|
:ok
|
|
_ -> :ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Notifies a player with a system message.
|
|
"""
|
|
def notify_player(character_id, message) do
|
|
drop_message(character_id, 5, message)
|
|
end
|
|
|
|
@doc """
|
|
Requests a screenshot from a player.
|
|
"""
|
|
def request_screenshot(character_id, session_key) do
|
|
case Players.get_player(character_id) do
|
|
nil -> :ok
|
|
%{client_pid: client_pid} when is_pid(client_pid) ->
|
|
packet = Packets.screenshot_request(session_key)
|
|
send(client_pid, {:send_packet, packet})
|
|
:ok
|
|
_ -> :ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Starts a lie detector on a player.
|
|
"""
|
|
def start_lie_detector(character_id) do
|
|
case Players.get_player(character_id) do
|
|
nil -> :ok
|
|
%{client_pid: client_pid} when is_pid(client_pid) ->
|
|
packet = Packets.start_lie_detector()
|
|
send(client_pid, {:send_packet, packet})
|
|
:ok
|
|
_ -> :ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Schedules a server shutdown.
|
|
"""
|
|
def schedule_shutdown(minutes) do
|
|
# Broadcast warning message to all players
|
|
broadcast_server_message("Server will shutdown in #{minutes} minutes.")
|
|
|
|
# Schedule the actual shutdown
|
|
Process.send_after(self(), :do_shutdown, minutes * 60 * 1000)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Initiates immediate shutdown.
|
|
"""
|
|
def initiate_shutdown do
|
|
broadcast_server_message("Server is shutting down now!")
|
|
|
|
# Disconnect all players
|
|
Players.clear()
|
|
|
|
# Signal application to stop
|
|
System.stop(0)
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts a message to all players on all channels.
|
|
"""
|
|
def broadcast_server_message(message) do
|
|
# In a full implementation, this would use Redis pub/sub
|
|
# For now, broadcast to all local players
|
|
Players.get_all_players()
|
|
|> Enum.each(fn %{client_pid: pid} ->
|
|
if is_pid(pid), do: send(pid, {:send_packet, Packets.server_message(message)})
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Gets a list of available commands for help display.
|
|
"""
|
|
def list_commands do
|
|
[
|
|
{"ban", "<player> [reason] [hell]", "Ban a player"},
|
|
{"dc", "<player>", "Disconnect a player"},
|
|
{"dcall", "", "Disconnect all players"},
|
|
{"dcchannel", "<channel>", "Disconnect all players in a channel"},
|
|
{"warp", "<player> <map_id>", "Warp player to map"},
|
|
{"dropmsg", "<player> <type> <message>", "Send drop message to player"},
|
|
{"slidemsg", "<message>", "Set scrolling server message"},
|
|
{"screen", "<player>", "Request screenshot from player"},
|
|
{"vote", "<account_id> <name> <result>", "Process vote reward"},
|
|
{"liedetector", "<player>", "Start lie detector on player"},
|
|
{"reload", "", "Reload configuration"},
|
|
{"shutdown", "[minutes]", "Graceful server shutdown"}
|
|
]
|
|
end
|
|
end
|