Files
odinsea-elixir/lib/odinsea/admin/commands.ex
2026-02-14 23:12:33 -07:00

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