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 Broadcast chat to all players", " msg Send private message", " changelevel Load a new map", " kick [reason] Remove a player", " ban [duration] Ban a player from the server", " unban Remove a ban", " exec Execute a server config file", " cvarlist [pattern] List cvars (optional filter)", " cvar_get Get a cvar's value", " cvar_set Set a writable cvar", " plugin [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 \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 \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"