kimi gone wild

This commit is contained in:
ra
2026-02-14 23:12:33 -07:00
parent bbd205ecbe
commit 0222be36c5
98 changed files with 39726 additions and 309 deletions

View 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

View 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