defmodule Odinsea.Admin.Commands do @moduledoc """ Admin command implementations. Ported from Java handling.admin.handler.* Commands: - !ban [reason] [hell] - Ban a player - !dc - Disconnect a player - !dcall - Disconnect all players - !dcchannel - Disconnect all players in a channel - !warp - Warp player to map - !dropmsg - Send drop message to player - !slidemsg - Set scrolling server message - !screen - Request screenshot from player - !vote - Process vote reward - !liedetector - 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 [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 "} 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 "} 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 or !warp "} 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 "} end end # Set scrolling server message defp do_execute("slidemsg", args, _admin_state) do case args do [] -> {:error, "Usage: !slidemsg "} _ -> 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 "} 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 "} 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 "} 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", " [reason] [hell]", "Ban a player"}, {"dc", "", "Disconnect a player"}, {"dcall", "", "Disconnect all players"}, {"dcchannel", "", "Disconnect all players in a channel"}, {"warp", " ", "Warp player to map"}, {"dropmsg", " ", "Send drop message to player"}, {"slidemsg", "", "Set scrolling server message"}, {"screen", "", "Request screenshot from player"}, {"vote", " ", "Process vote reward"}, {"liedetector", "", "Start lie detector on player"}, {"reload", "", "Reload configuration"}, {"shutdown", "[minutes]", "Graceful server shutdown"} ] end end