159c554a86
- Add PluginBase (plugin_base.gd) — abstract base class with 12 virtual hooks - Add PluginManifest (plugin_manifest.gd) — custom Resource for plugin metadata - Add PluginManager (plugin_manager.gd) — autoload singleton: directory scan, hook dispatch, lifecycle (load/unload/reload/rescan), cvar integration - Add hello_world example plugin with all hooks demonstrating usage - Register PluginManager autoload in project.godot - Extend rcon_command_handler.gd with 'plugin' subcommand (list/load/unload/ reload/info/rescan) delegating to PluginManager
245 lines
9.2 KiB
GDScript
245 lines
9.2 KiB
GDScript
extends Node
|
|
|
|
# RCON Admin Console — Command Handler
|
|
# ============================================================================
|
|
# Processes RCON commands, returns formatted response strings.
|
|
# Game-affecting commands (changelevel, kick, ban, say, players, etc.)
|
|
# are dispatched via the `rcon_command` signal so game logic can handle them
|
|
# without coupling RCON to game-specific systems.
|
|
#
|
|
# Commands handled inline (no game dependency):
|
|
# help, echo/ping, status (engine-level stats), quit
|
|
# cvarlist, cvar_get, cvar_set (via CvarRegistry singleton if available)
|
|
#
|
|
# Plugin commands are delegated to PluginManager singleton:
|
|
# plugin list, plugin load, plugin unload, plugin reload,
|
|
# plugin info, plugin rescan
|
|
#
|
|
# Commands dispatched via signal:
|
|
# changelevel, kick, ban, unban, say, msg, players, exec
|
|
#
|
|
# Connection pattern for game logic:
|
|
# func _ready():
|
|
# var rcon = get_node("/root/RconServer/RconCommandHandler")
|
|
# if rcon:
|
|
# rcon.rcon_command.connect(_on_rcon_command)
|
|
#
|
|
# func _on_rcon_command(command: String, args: PackedStringArray):
|
|
# match command:
|
|
# "changelevel":
|
|
# $ServerManager.change_level(args[0])
|
|
# "kick":
|
|
# $PlayerManager.kick_player(args[0])
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signals
|
|
# ---------------------------------------------------------------------------
|
|
signal rcon_command(command: String, args: PackedStringArray)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API — called by rcon_server.gd
|
|
# ---------------------------------------------------------------------------
|
|
func handle_command(cmd: String, args: PackedStringArray) -> String:
|
|
match cmd:
|
|
"help":
|
|
return _cmd_help(args)
|
|
"echo", "ping":
|
|
return _cmd_echo(args)
|
|
"status":
|
|
return _cmd_status(args)
|
|
"quit":
|
|
return _cmd_quit(args)
|
|
"cvarlist":
|
|
return _cmd_cvarlist(args)
|
|
"cvar_get":
|
|
return _cmd_cvar_get(args)
|
|
"cvar_set":
|
|
return _cmd_cvar_set(args)
|
|
"plugin":
|
|
return _cmd_plugin(args)
|
|
"say", "msg", "players", "changelevel", "kick", "ban", "unban", "exec":
|
|
return _cmd_dispatch(cmd, args)
|
|
_:
|
|
return "Unknown command: '" + cmd + "'. Type 'help' for available commands.\r\n"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Built-in commands
|
|
# ---------------------------------------------------------------------------
|
|
func _cmd_help(_args: PackedStringArray) -> String:
|
|
var lines: PackedStringArray = [
|
|
"=== RCON Commands ===",
|
|
" help Show this help",
|
|
" echo [text] Echo text back (ping test)",
|
|
" status Show server engine status",
|
|
" players List connected players",
|
|
" say <message> Broadcast chat to all players",
|
|
" msg <player> <message> Send private message",
|
|
" changelevel <map_name> Load a new map",
|
|
" kick <player> [reason] Remove a player",
|
|
" ban <player> [duration] Ban a player from the server",
|
|
" unban <player_or_steamid> Remove a ban",
|
|
" exec <config_file> Execute a server config file",
|
|
" cvarlist [pattern] List cvars (optional filter)",
|
|
" cvar_get <name> Get a cvar's value",
|
|
" cvar_set <name> <value> Set a writable cvar",
|
|
" plugin <subcommand> [args] Plugin management (list/load/unload/reload/info/rescan)",
|
|
" quit Shut down the server gracefully",
|
|
]
|
|
return "\r\n".join(lines) + "\r\n"
|
|
|
|
func _cmd_echo(args: PackedStringArray) -> String:
|
|
if args.is_empty():
|
|
return "Pong\r\n"
|
|
return args.join(" ") + "\r\n"
|
|
|
|
func _cmd_status(_args: PackedStringArray) -> String:
|
|
var lines: PackedStringArray = []
|
|
lines.append("=== Server Status ===")
|
|
|
|
# Engine version
|
|
var vi: Dictionary = Engine.get_version_info()
|
|
lines.append(" Engine: Godot " + str(vi.get("major", "?")) + "." + str(vi.get("minor", "?")) + "." + str(vi.get("patch", "?")))
|
|
|
|
# FPS
|
|
lines.append(" FPS: " + str(Engine.get_frames_per_second()))
|
|
|
|
# Physics tick rate
|
|
lines.append(" Tickrate: " + str(Engine.physics_ticks_per_second) + " Hz")
|
|
|
|
# Uptime
|
|
var uptime_ms: int = Time.get_ticks_msec()
|
|
var total_sec: int = uptime_ms / 1000
|
|
var h: int = total_sec / 3600
|
|
var m: int = (total_sec % 3600) / 60
|
|
var s: int = total_sec % 60
|
|
lines.append(" Uptime: " + str(h) + "h " + str(m) + "m " + str(s) + "s")
|
|
|
|
# Server time
|
|
var dt: Dictionary = Time.get_datetime_dict_from_system()
|
|
lines.append(" Time: " + \
|
|
str(dt.get("year", 0)) + "-" + str(dt.get("month", 1)).pad_zeros(2) + "-" + str(dt.get("day", 1)).pad_zeros(2) + " " + \
|
|
str(dt.get("hour", 0)).pad_zeros(2) + ":" + str(dt.get("minute", 0)).pad_zeros(2) + ":" + str(dt.get("second", 0)).pad_zeros(2))
|
|
|
|
# Performance metrics
|
|
lines.append(" Objects: " + str(Performance.get_monitor(Performance.OBJECT_COUNT)))
|
|
var mem_kb: float = Performance.get_monitor(Performance.MEMORY_STATIC)
|
|
lines.append(" Memory: " + str(snapped(mem_kb / 1024.0, 0.1)) + " MB")
|
|
|
|
# Node count
|
|
lines.append(" Nodes: " + str(Performance.get_monitor(Performance.NODE_COUNT)))
|
|
|
|
# RCON info
|
|
lines.append(" RCON: enabled on port ? (see rcon_server)")
|
|
|
|
# Try to get server name from engine
|
|
var server_name: String = ProjectSettings.get_setting("application/config/name", "Tactical Shooter")
|
|
lines.append(" Server: " + server_name)
|
|
|
|
return "\r\n".join(lines) + "\r\n"
|
|
|
|
func _cmd_quit(_args: PackedStringArray) -> String:
|
|
# Defer shutdown to next frame so the response can be sent
|
|
get_tree().call_deferred("quit")
|
|
return "Server shutting down...\r\n"
|
|
|
|
func _cmd_cvarlist(args: PackedStringArray) -> String:
|
|
var pattern: String = args[0] if args.size() > 0 else ""
|
|
|
|
if not has_node("/root/CvarRegistry"):
|
|
if pattern.is_empty() or "rcon" in pattern:
|
|
return "rcon_enabled: bool\r\nrcon_port: int\r\nrcon_password: string [PW]\r\n(Cvar registry not available — RCON cvars shown)\r\n"
|
|
return "Cvar registry not available. No cvars match '" + pattern + "'.\r\n"
|
|
|
|
var registry = get_node("/root/CvarRegistry")
|
|
if not registry or not registry.has_method("list_cvars"):
|
|
return "Cvar registry available but incompatible.\r\n"
|
|
|
|
var cvar_names: Array = registry.list_cvars()
|
|
var lines: PackedStringArray = []
|
|
lines.append("=== Cvar List" + (" (filter: '" + pattern + "')" if pattern else "") + " ===")
|
|
|
|
var found: int = 0
|
|
for cvar_name in cvar_names:
|
|
if not pattern.is_empty() and pattern not in str(cvar_name):
|
|
continue
|
|
|
|
var val = registry.get(cvar_name)
|
|
var flags: int = 0
|
|
var cvar_type: String = "?"
|
|
|
|
if registry.has_method("get_flags"):
|
|
flags = registry.get_flags(cvar_name)
|
|
if registry.has_method("get_type"):
|
|
cvar_type = str(registry.get_type(cvar_name))
|
|
|
|
var readonly: String = " [RO]" if (flags & 1) else ""
|
|
var passwd: String = " [PW]" if str(cvar_name).find("password") >= 0 else ""
|
|
lines.append(" " + str(cvar_name) + " = " + str(val) + " (" + cvar_type + ")" + readonly + passwd)
|
|
found += 1
|
|
|
|
if found == 0:
|
|
return "No cvars match '" + pattern + "'.\r\n"
|
|
|
|
lines.append(str(found) + " cvars listed.")
|
|
return "\r\n".join(lines) + "\r\n"
|
|
|
|
func _cmd_cvar_get(args: PackedStringArray) -> String:
|
|
if args.is_empty():
|
|
return "Usage: cvar_get <name>\r\n"
|
|
|
|
var name: String = args[0]
|
|
|
|
if has_node("/root/CvarRegistry"):
|
|
var registry = get_node("/root/CvarRegistry")
|
|
if registry and registry.has_method("get"):
|
|
var val = registry.get(name)
|
|
if val != null:
|
|
return name + " = " + str(val) + "\r\n"
|
|
return "Cvar not found: " + name + "\r\n"
|
|
|
|
return "Cvar registry not available.\r\n"
|
|
|
|
func _cmd_cvar_set(args: PackedStringArray) -> String:
|
|
if args.size() < 2:
|
|
return "Usage: cvar_set <name> <value>\r\n"
|
|
|
|
var name: String = args[0]
|
|
var value: String = args[1]
|
|
|
|
if has_node("/root/CvarRegistry"):
|
|
var registry = get_node("/root/CvarRegistry")
|
|
if registry and registry.has_method("set"):
|
|
var err: int = registry.set(name, value)
|
|
if err == OK:
|
|
return name + " = " + value + "\r\n"
|
|
match err:
|
|
ERR_INVALID_PARAMETER:
|
|
return "Cvar not found or read-only: " + name + "\r\n"
|
|
ERR_INVALID_DATA:
|
|
return "Invalid value for " + name + ": " + value + "\r\n"
|
|
_:
|
|
return "Failed to set " + name + " (error " + str(err) + ")\r\n"
|
|
|
|
return "Cvar registry not available.\r\n"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plugin management — delegates to PluginManager singleton
|
|
# ---------------------------------------------------------------------------
|
|
func _cmd_plugin(args: PackedStringArray) -> String:
|
|
# Try to find PluginManager singleton (autoload)
|
|
var pm = get_node("/root/PluginManager")
|
|
if not pm:
|
|
return "PluginManager not available. Ensure PluginManager is registered as an autoload in project.godot.\r\n"
|
|
|
|
if not pm.has_method("rcon_command"):
|
|
return "PluginManager does not support RCON commands.\r\n"
|
|
|
|
return pm.rcon_command(args)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Delegated commands — emit signal for game logic to handle
|
|
# ---------------------------------------------------------------------------
|
|
func _cmd_dispatch(cmd: String, args: PackedStringArray) -> String:
|
|
rcon_command.emit(cmd, args)
|
|
return "Command '" + cmd + "' dispatched to server.\r\n"
|