kimi gone wild
This commit is contained in:
515
lib/odinsea/admin/commands.ex
Normal file
515
lib/odinsea/admin/commands.ex
Normal file
@@ -0,0 +1,515 @@
|
||||
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
|
||||
150
lib/odinsea/admin/handler.ex
Normal file
150
lib/odinsea/admin/handler.ex
Normal file
@@ -0,0 +1,150 @@
|
||||
defmodule Odinsea.Admin.Handler do
|
||||
@moduledoc """
|
||||
Main admin command handler.
|
||||
Ported from Java handling.admin.AdminHandler
|
||||
|
||||
Parses chat messages starting with '!' as admin commands
|
||||
and routes them to the appropriate command implementation.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias Odinsea.Admin.Commands
|
||||
alias Odinsea.Channel.Packets
|
||||
|
||||
@doc """
|
||||
Parses and executes an admin command from chat message.
|
||||
|
||||
Commands start with '!' followed by the command name and arguments.
|
||||
Example: "!warp PlayerName 100000000"
|
||||
|
||||
Returns:
|
||||
- {:ok, result_message} - Command executed successfully
|
||||
- {:error, reason} - Command failed
|
||||
- :not_command - Message is not an admin command
|
||||
"""
|
||||
def handle_command(message, client_state) when is_binary(message) do
|
||||
# Check if message is a command (starts with !)
|
||||
if String.starts_with?(message, "!") do
|
||||
parse_and_execute(message, client_state)
|
||||
else
|
||||
:not_command
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses command string and executes.
|
||||
"""
|
||||
def parse_and_execute(message, client_state) do
|
||||
# Remove leading '!' and split into command and arguments
|
||||
command_str = String.slice(message, 1..-1//-1)
|
||||
parts = String.split(command_str)
|
||||
|
||||
case parts do
|
||||
[] ->
|
||||
{:error, "Empty command"}
|
||||
|
||||
[command | args] ->
|
||||
command = String.downcase(command)
|
||||
|
||||
char_id = if client_state.character_id, do: client_state.character_id, else: "unknown"
|
||||
Logger.info("Admin command from #{char_id}: #{command} #{inspect(args)}")
|
||||
|
||||
# Get admin state (character info with GM level)
|
||||
admin_state = build_admin_state(client_state)
|
||||
|
||||
case Commands.execute(command, args, admin_state) do
|
||||
{:ok, result} ->
|
||||
# Send success message back to admin
|
||||
notify_admin(client_state, result)
|
||||
{:ok, result}
|
||||
|
||||
{:error, reason} ->
|
||||
# Send error message back to admin
|
||||
notify_admin(client_state, "Error: #{reason}")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a message is an admin command.
|
||||
"""
|
||||
def admin_command?(message) do
|
||||
String.starts_with?(message, "!")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the command name from a message (for logging).
|
||||
"""
|
||||
def extract_command_name(message) do
|
||||
case String.split(message) do
|
||||
[first | _] -> String.downcase(String.trim_leading(first, "!"))
|
||||
_ -> "unknown"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends help information to the admin.
|
||||
"""
|
||||
def send_help(client_state) do
|
||||
commands = Commands.list_commands()
|
||||
|
||||
help_text = [
|
||||
"=== Admin Commands ===",
|
||||
""
|
||||
| Enum.map(commands, fn {cmd, args, desc} ->
|
||||
"!#{cmd} #{args} - #{desc}"
|
||||
end)
|
||||
]
|
||||
|> Enum.join("\n")
|
||||
|
||||
notify_admin(client_state, help_text)
|
||||
end
|
||||
|
||||
# ============================================================================
|
||||
# Private Functions
|
||||
# ============================================================================
|
||||
|
||||
defp build_admin_state(client_state) do
|
||||
# Get character information including GM level
|
||||
gm_level = get_gm_level(client_state)
|
||||
|
||||
%{
|
||||
character_id: client_state.character_id,
|
||||
channel_id: client_state.channel_id,
|
||||
gm_level: gm_level,
|
||||
client_pid: self()
|
||||
}
|
||||
end
|
||||
|
||||
defp get_gm_level(client_state) do
|
||||
# Try to get GM level from character
|
||||
case client_state.character_id do
|
||||
nil -> 0
|
||||
character_id ->
|
||||
# In a full implementation, this would query the character state
|
||||
# For now, use a default or check player storage
|
||||
case Odinsea.Channel.Players.get_player(character_id) do
|
||||
nil -> 0
|
||||
player_data -> Map.get(player_data, :gm, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp notify_admin(client_state, message) do
|
||||
case client_state.character_id do
|
||||
nil ->
|
||||
:ok
|
||||
character_id ->
|
||||
case Odinsea.Channel.Players.get_player(character_id) do
|
||||
nil -> :ok
|
||||
%{client_pid: pid} when is_pid(pid) ->
|
||||
packet = Packets.drop_message(5, message)
|
||||
send(pid, {:send_packet, packet})
|
||||
:ok
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user