diff --git a/.gitignore b/.gitignore index d6caa63..d2bd6da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ __pycache__/ venv3/ +*test* *.json *.log cvarlist.txt -curztest.py +qlpycon.conf rconpw.txt -CLAUDE*.md diff --git a/README.md b/README.md index 2e21a83..f8c7977 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,80 @@ -# QLPyCon - Quake Live Python Console +# qlpycon - Quake Live Python Console -Terminal-based client for monitoring and controlling Quake Live servers via ZMQ. - -## Features - -- Real-time game monitoring (kills, deaths, medals, team switches) -- Server info display (map, gametype, scores, player list) -- Team-aware colorized chat with location tracking -- Powerup pickup and carrier kill notifications -- JSON event capture for analysis -- Quake color code support (^0-^7) -- **Autocomplete for cvars/commands** with fuzzy matching -- **Intelligent argument suggestions** for 25+ commands (bot names, maps, gametypes) +Terminal client for monitoring and controlling Quake Live servers via ZMQ RCON. ## Installation ```bash -pip install pyzmq -python main.py --host tcp://SERVER_IP:PORT --password RCON_PASSWORD +curl -sSL https://6bit.ch/qlpycon/install.sh | bash +``` + +Or from a cloned repo: + +```bash +git clone https://git.6bit.ch/xbl/qlpycon.git +cd qlpycon +./install.sh +``` + +The installer sets up a virtualenv, installs dependencies, and writes a launcher to `~/.local/bin/qlpycon`. + +## Configuration + +Edit `qlpycon.conf` in your install directory. See `qlpycon.conf.example` for all options. + +```ini +[connection] +password = ${QLPYCON_PASSWORD} + +[servers] +ffa = 10.13.12.93:28960 +duel = 10.13.12.93:28961 ``` ## Usage ```bash -# Basic -python main.py --host tcp://SERVER_IP:PORT --password RCON_PASSWORD - -# Verbose logging -python main.py --host tcp://SERVER_IP:PORT --password RCON_PASSWORD -v - -# Capture JSON events -python main.py --host tcp://SERVER_IP:PORT --password RCON_PASSWORD --json events.log +qlpycon ffa # connect by name +qlpycon --host tcp://10.13.12.93:28960 --password secret # connect directly +qlpycon --list # list configured servers ``` **Options:** -- `--host URI` - ZMQ RCON endpoint (default: tcp://127.0.0.1:27961) -- `--password PASS` - RCON password (required) -- `-v` / `-vv` - Verbose (INFO) or debug (DEBUG) logging -- `--json FILE` - Log all events as JSON -- `--unknown-log FILE` - Log unparsed events (default: unknown_events.log) +- `--host URI` — ZMQ RCON endpoint +- `--password PASS` — RCON password (or set `QLPYCON_PASSWORD` env var) +- `--list` — list configured servers and exit +- `-v` / `-vv` — verbose (INFO) or debug (DEBUG) logging +- `--json FILE` — log all JSON events to file +- `--unknown-log FILE` — log unparsed events (default: unknown_events.log) -**Configuration File (Optional):** -Create `~/.qlpycon.conf` or `./qlpycon.conf`: -```ini -[connection] -host = tcp://SERVER_IP:PORT -password = your_password +## Features -[logging] -level = INFO -``` +- Real-time kill/death/medal/team switch events +- Team-aware colorized output with Quake color code support +- Powerup pickup and carrier kill notifications +- Server info panel (map, gametype, scores, players) +- Tab autocomplete for cvars and commands with fuzzy matching +- Argument suggestions for 25+ commands (bot names, maps, gametypes) +- Command history (↑/↓) -**Input Features:** -- **Smart autocomplete** - Type commands and see arguments highlighted in real-time -- **Argument value suggestions** - Intelligent suggestions for bot names, maps, gametypes, teams, etc. -- **Tab** - Cycle through autocomplete suggestions -- **↑/↓** - Command history navigation -- **Argument highlighting** - Current argument position shown in reverse video -- **Command signatures** - Automatic display (e.g., `addbot [skill 1-5] [team]`) - -See [AUTOCOMPLETE.md](AUTOCOMPLETE.md) for details. +See [AUTOCOMPLETE.md](AUTOCOMPLETE.md) for input details. ## Architecture ``` -main.py - Main loop, argument parsing, signal handling -config.py - Constants (weapons, teams, colors, limits) -state.py - Game state (ServerInfo, PlayerTracker, EventDeduplicator) -network.py - ZMQ connections (RCON DEALER, Stats SUB sockets) -parser.py - JSON event parsing (deaths, medals, switches, stats) -formatter.py - Message formatting, color codes, team prefixes -ui.py - Curses interface (3-panel: info, output, input) +main.py — entry point, arg parsing, signal handling +qlpycon.conf — user configuration +lib/ + constants.py — weapons, teams, colors, limits + settings.py — config file loader + state.py — game state (server info, players, teams) + network.py — ZMQ connections (RCON DEALER, stats SUB) + parser.py — JSON event parsing + formatter.py — message formatting and colorization + ui.py — curses interface (info / output / input panels) + cvars.py — cvar/command database and autocomplete ``` -**Supported Events:** -PLAYER_SWITCHTEAM, PLAYER_DEATH/KILL, PLAYER_MEDAL, PLAYER_STATS, MATCH_STARTED/REPORT, PLAYER_CONNECT/DISCONNECT, ROUND_OVER - ## License WTFPL diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..49e0a5e --- /dev/null +++ b/install.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# +# qlpycon installer +# Usage: +# curl -sSL https://6bit.ch/qlpycon/install.sh | bash +# +# or run directly from cloned repo: +# git clone https://git.6bit.ch/xbl/qlpycon.git +# cd qlpycon +# chmod u+x install.sh +# ./install.sh +# + +set -e + +TAR_URL="https://6bit.ch/qlpycon/qlpycon.tar.gz" +BIN_DIR="$HOME/.local/bin" +BIN_PATH="$BIN_DIR/qlpycon" + +# === Colors ===> +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' + +info() { echo -e "${CYAN} o_o [INFO]${NC} $*"; } +success() { echo -e "${GREEN} ^_^ [SUCCESS]${NC} $*"; } +warn() { echo -e "${YELLOW} >_< [WARN]${NC} $*"; } +die() { echo -e "${RED} x_x [ERROR]${NC} $*" >&2; exit 1; } + +# === Checks ===> +check_dependencies() { + info "Checking dependencies..." + + command -v python3 >/dev/null 2>&1 || die "python3 is required but not installed" + + # Check Python version >= 3.9 + python3 -c " +import sys +if sys.version_info < (3, 9): + print('Python 3.9+ required, found ' + sys.version) + sys.exit(1) +" || die "Python 3.9 or higher is required" + + command -v pip3 >/dev/null 2>&1 || \ + python3 -m pip --version >/dev/null 2>&1 || \ + die "pip is required but not installed" + + success "Dependencies OK" +} + +# === Source detection ===> +detect_source() { + if [[ -f "./main.py" && -d "./lib" ]]; then + echo "repo" + else + echo "download" + fi +} + +# === Download ===> +download_and_extract() { + info "Downloading to temp and extracting to $INSTALL_DIR..." + + mkdir -p "$INSTALL_DIR" + + TMP_TAR=$(mktemp) + + if command -v curl >/dev/null 2>&1; then + curl -sSL "$TAR_URL" -o "$TMP_TAR" || die "Download failed" + else + wget -qO "$TMP_TAR" "$TAR_URL" || die "Download failed" + fi + + # Preserve existing config if updating + if [[ -f "$INSTALL_DIR/qlpycon.conf" ]]; then + warn "Existing qlpycon.conf found - keeping it" + tar -xzf "$TMP_TAR" -C "$INSTALL_DIR" --exclude='qlpycon.conf' + else + tar -xzf "$TMP_TAR" -C "$INSTALL_DIR" + fi + + rm -f "$TMP_TAR" + + success "Extracted to $INSTALL_DIR" +} + +# === Venv ===> +setup_venv() { + info "Setting up Python virtual environment..." + + if [[ ! -d "$INSTALL_DIR/venv" ]]; then + python3 -m venv "$INSTALL_DIR/venv" || die "Failed to create venv" + else + warn "venv already exists - skipping creation" + fi + + info "Installing pip" + "$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade pip + info "Installing pyzmq via pip" + "$INSTALL_DIR/venv/bin/pip" install --quiet pyzmq + + success "Virtual environment ready" +} + +# === Launcher ===> +write_launcher() { + info "Writing launcher to $BIN_PATH..." + + mkdir -p "$BIN_DIR" + + cat > "$BIN_PATH" << EOF +#!/usr/bin/env bash +# qlpycon launcher — generated by install.sh +QLPYCON_DIR="$INSTALL_DIR" + +if [[ ! -d "\$QLPYCON_DIR" ]]; then + echo "Error: qlpycon directory not found: \$QLPYCON_DIR" + echo "Re-run install.sh or cry :(" + exit 1 +fi + +cd "\$QLPYCON_DIR" +source "\$QLPYCON_DIR/venv/bin/activate" +exec python3 main.py "\$@" +EOF + + chmod +x "$BIN_PATH" + success "Launcher written to $BIN_PATH" +} + +# === PATH check ===> +check_path() { + info "Checking \$PATH..." + if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then + warn "$BIN_DIR is not in your PATH." + echo "" + echo " Add this to your ~/.bashrc or ~/.zshrc:" + echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" + echo "" + else + success "\$PATH is superfine" + fi +} + +# === Config ===> +setup_config() { + if [[ ! -f "$INSTALL_DIR/qlpycon.conf" ]]; then + if [[ -f "$INSTALL_DIR/qlpycon.conf.example" ]]; then + cp "$INSTALL_DIR/qlpycon.conf.example" "$INSTALL_DIR/qlpycon.conf" + success "Created $INSTALL_DIR/qlpycon.conf from example" + fi + fi +} + +# === Main ===> + +main() { + print_banner() { + cat <<'EOF' + _/ + _/_/_/ _/ _/_/_/ _/ _/ _/_/_/ _/_/ _/_/_/ + _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ + _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ + _/_/_/ _/ _/_/_/ _/_/_/ _/_/_/ _/_/ _/ _/ + _/ _/ _/ + _/ _/ _/_/ +EOF + } + + if command -v lolcat >/dev/null 2>&1 && [[ ${LOLCAT:-1} -eq 1 ]]; then + { + print_banner + echo + echo " qlpycon installer" + } | lolcat - + else + echo -e "${MAGENTA}" + print_banner + echo -e "${NC}" + echo -e "${CYAN} qlpycon installer${NC}" + fi + + echo + + check_dependencies + + SOURCE=$(detect_source) + + if [[ "$SOURCE" == "repo" ]]; then + INSTALL_DIR="$(pwd)" + info "Running from cloned repo - using $INSTALL_DIR" + else + DEFAULT_DIR="$HOME/.local/share/qlpycon" + read -p "Install directory [$DEFAULT_DIR]: " USER_DIR host:port from [servers]""" + if not self.config.has_section('servers'): + return {} + return dict(self.config.items('servers')) + + def get_server(self, name): + """ + Resolve a named server to (host, password). + host comes from [servers], password from [server:name] or [connection]. + Returns (host, password) or (None, None) if name not found. + """ + servers = self.get_servers() + if name not in servers: + return None, None + + host = servers[name] + if not host.startswith('tcp://'): + host = f'tcp://{host}' + + # Per-server password override + section = f'server:{name}' + if self.config.has_section(section): + password = self.config.get(section, 'password', fallback=None) + if password: + password = self._resolve_password(password) + else: + password = self.get_password() + + return host, password + + def _resolve_password(self, password): + """Resolve a password string, expanding ${VAR:-default} if needed""" + if not password: + return None + if password.startswith('${') and password.endswith('}'): + inner = password[2:-1] + if ':-' in inner: + env_var, default = inner.split(':-', 1) + else: + env_var, default = inner, None + return os.environ.get(env_var, default) return password def get_log_level(self): @@ -104,29 +143,52 @@ class ConfigLoader: def create_example_config(): """Create an example configuration file""" - config_content = """# QLPyCon Configuration File -# Place this file as ~/.qlpycon.conf or ./qlpycon.conf + config_content = """# qlpycon.conf +# Edit this file as needed. +# +# Connect by server name: qlpycon ffa +# Connect directly: qlpycon --host tcp://1.2.3.4:28960 --password secret +# List servers: qlpycon --list +# === Connection defaults ===> +# Default host if no server name or --host is given [connection] -# Server connection settings -host = tcp://10.13.12.93:28969 -# Use ${ENV_VAR} to read from environment -password = ${QLPYCON_PASSWORD} +host = tcp://127.0.0.1:28960 +# Password for all servers unless overridden in [server:name] +# Use ${ENV_VAR} to read from environment variable (recommended) +# Or set directly (less secure): +password = ${QLPYCON_PASSWORD:-secret} + +# === Named servers ===> +# Simple entries: name = host:port +# These use the password from [connection] above. +[servers] +# Example: +#ffa = 10.13.12.161:28960 + +# === Per-server overrides ===> +# Use [server:name] to override any setting for a specific server. +# The name must match an entry in [servers] above. +# Example: +#[server:ffa] +#password = ${FFA_PASSWORD:-secret} + +# === Logging ===> [logging] # Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL -level = INFO +level = WARNING +# === UI ===> [ui] -# Max command history entries +# Number of commands to remember in history max_history = 10 -# Color scheme (future feature) -color_scheme = quake +# === Behaviour ===> [behavior] -# Quit confirmation timeout (seconds) +# Seconds to confirm quit (press Ctrl-C twice within this window) quit_timeout = 3.0 -# Player respawn delay (seconds) +# Seconds before players respawn after death respawn_delay = 3.0 """ diff --git a/state.py b/lib/state.py similarity index 98% rename from state.py rename to lib/state.py index d6dd7d4..d5ef5ef 100644 --- a/state.py +++ b/lib/state.py @@ -5,8 +5,8 @@ Tracks server info, players, and teams """ import logging -from config import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS -from formatter import strip_color_codes +from .constants import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS +from .formatter import strip_color_codes logger = logging.getLogger('state') diff --git a/ui.py b/lib/ui.py similarity index 98% rename from ui.py rename to lib/ui.py index d9f82f5..4132843 100644 --- a/ui.py +++ b/lib/ui.py @@ -10,8 +10,8 @@ import threading import queue import logging import time -from config import COLOR_PAIRS, INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, TEAM_MODES, MAX_COMMAND_HISTORY -from cvars import autocomplete, COMMAND_SIGNATURES, get_signature_with_highlight, get_argument_suggestions, COMMAND_ARGUMENTS +from .constants import COLOR_PAIRS, INFO_WINDOW_HEIGHT, INFO_WINDOW_Y, OUTPUT_WINDOW_Y, INPUT_WINDOW_HEIGHT, TEAM_MODES, MAX_COMMAND_HISTORY +from .cvars import autocomplete, COMMAND_SIGNATURES, get_signature_with_highlight, get_argument_suggestions, COMMAND_ARGUMENTS logger = logging.getLogger('ui') @@ -729,7 +729,7 @@ class UIManager: red = f"{red_score:>3} {'^8^1X^7^0 ' if red_dead else ''}{red_name}" if red_name else '' blue = f"{blue_score:>3} {'^8^1X^7^0 ' if blue_dead else ''}{blue_name}" if blue_name else '' - from formatter import strip_color_codes + from .formatter import strip_color_codes red_clean = strip_color_codes(red) blue_clean = strip_color_codes(blue) @@ -770,7 +770,7 @@ class UIManager: col1 = f"{col1_score:>3} {'^8^1X^7^0 ' if col1_dead else ''}{col1_name}" if col1_name else '' col2 = f"{col2_score:>3} {'^8^1X^7^0 ' if col2_dead else ''}{col2_name}" if col2_name else '' - from formatter import strip_color_codes + from .formatter import strip_color_codes col1_clean = strip_color_codes(col1) col2_clean = strip_color_codes(col2) diff --git a/main.py b/main.py index ced7b2e..4f1e800 100644 --- a/main.py +++ b/main.py @@ -16,13 +16,13 @@ import sys import os import threading -from config import VERSION, DEFAULT_HOST, POLL_TIMEOUT, QUIT_CONFIRM_TIMEOUT, RESPAWN_DELAY, MAX_COMMAND_HISTORY -from state import GameState -from network import RconConnection, StatsConnection -from parser import EventParser -from formatter import format_message, format_chat_message, format_powerup_message, strip_color_codes -from ui import UIManager -from qlpycon_config import ConfigLoader +from lib.constants import VERSION, DEFAULT_HOST, POLL_TIMEOUT, QUIT_CONFIRM_TIMEOUT, RESPAWN_DELAY, MAX_COMMAND_HISTORY +from lib.state import GameState +from lib.network import RconConnection, StatsConnection +from lib.parser import EventParser +from lib.formatter import format_message, format_chat_message, format_powerup_message, strip_color_codes +from lib.ui import UIManager +from lib.settings import ConfigLoader # Pre-compiled regex patterns CVAR_RESPONSE_PATTERN = re.compile(r'"([^"]+)"\s+is:"([^"]*)"') @@ -299,32 +299,12 @@ def parse_player_events(message, game_state, ui): return False -def main_loop(screen): +def main_loop(screen, args): """Main application loop""" # Setup signal handler for Ctrl+C with confirmation signal.signal(signal.SIGINT, signal_handler) - # Load configuration file (optional) - config = ConfigLoader() - config.load() - - # Parse arguments (command line overrides config file) - parser = argparse.ArgumentParser(description='Verbose QuakeLive server statistics') - parser.add_argument('--host', default=config.get_host() or DEFAULT_HOST, - help=f'ZMQ URI to connect to. Defaults to {DEFAULT_HOST}') - parser.add_argument('--password', default=config.get_password(), required=False, - help='RCON password') - parser.add_argument('--identity', default=uuid.uuid1().hex, - help='Socket identity (random UUID by default)') - parser.add_argument('-v', '--verbose', action='count', default=0, - help='Increase verbosity (-v INFO, -vv DEBUG)') - parser.add_argument('--unknown-log', default='unknown_events.log', - help='File to log unknown JSON events') - parser.add_argument('-j', '--json', dest='json_log', default=None, - help='File to log all JSON events') - args = parser.parse_args() - # Set logging level if args.verbose == 0: logger.setLevel(logging.WARNING) @@ -356,6 +336,7 @@ def main_loop(screen): ui.print_message(f"zmq python bindings {zmq.__version__}, libzmq version {zmq.zmq_version()}\n") # Initialize network connections + ui.print_message(f"Connecting with host={args.host} password={args.password}\n") rcon = RconConnection(args.host, args.password, args.identity) rcon.connect() @@ -546,4 +527,52 @@ def main_loop(screen): logger.info("Shutdown complete") if __name__ == '__main__': - curses.wrapper(main_loop) + # Load config + config = ConfigLoader() + config.load() + + # Parse arguments + parser = argparse.ArgumentParser(description='Quake Live Python Console') + parser.add_argument('server', nargs='?', default=None, + help='Named server from qlpycon.conf (e.g. ffa, duel)') + parser.add_argument('--host', default=None, + help='ZMQ URI to connect to (e.g. tcp://1.2.3.4:28960)') + parser.add_argument('--password', default=None, + help='RCON password') + parser.add_argument('--list', action='store_true', + help='List configured servers and exit') + parser.add_argument('--identity', default=uuid.uuid1().hex, + help='Socket identity (random UUID by default)') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='Increase verbosity (-v INFO, -vv DEBUG)') + parser.add_argument('--unknown-log', default='unknown_events.log', + help='File to log unknown JSON events') + parser.add_argument('-j', '--json', dest='json_log', default=None, + help='File to log all JSON events') + args = parser.parse_args() + + # Handle --list + if args.list: + servers = config.get_servers() + if not servers: + print('No servers configured in qlpycon.conf') + else: + print('Configured servers:') + for name, host in servers.items(): + print(f' {name:<12} {host}') + sys.exit(0) + + # Resolve host and password + if args.server: + host, password = config.get_server(args.server) + if host is None: + print(f"Error: server '{args.server}' not found in qlpycon.conf") + print("Use 'qlpycon --list' to see configured servers.") + sys.exit(1) + args.host = args.host or host + args.password = args.password or password + else: + args.host = args.host or config.get_host() or DEFAULT_HOST + args.password = args.password or config.get_password() + + curses.wrapper(main_loop, args) diff --git a/qlpycon.bash b/qlpycon.bash index f79ada3..d12516b 100755 --- a/qlpycon.bash +++ b/qlpycon.bash @@ -1,66 +1,13 @@ #!/usr/bin/env bash -# -# Helper script to connect to different Quake Live servers -# Required: QLPYCON_PASSWORD -# Optional: QLPYCON_WORKDIR (default: /home/marc/git/qlpycon.git) -# QLPYCON_SERVERIP (default: 10.13.12.93) +# qlpycon launcher — generated by install.sh +QLPYCON_DIR="/home/xbl/gitz/qlpycon" -# Required -password="${QLPYCON_PASSWORD:-}" -workdir="${QLPYCON_WORKDIR:-/home/xbl/gitz/qlpycon}" -serverip="${QLPYCON_SERVERIP:-10.13.12.93}" - -# Validate -if [ -z "$password" ]; then - echo "Error: QLPYCON_PASSWORD not set" - echo "Set: export QLPYCON_PASSWORD='your_password'" +if [[ ! -d "$QLPYCON_DIR" ]]; then + echo "Error: qlpycon directory not found: $QLPYCON_DIR" + echo "Re-run install.sh or cry :(" exit 1 fi -if [ ! -d "$workdir" ]; then - echo "Error: Working directory does not exist: $workdir" - exit 1 -fi - -if [ ! -d "$workdir/venv" ]; then - echo "Error: venv not found in $workdir" - exit 1 -fi - -cd "$workdir" || exit 1 -source venv/bin/activate - -if [ "$1" == "ffa" ]; then - python3 main.py --host tcp://"$serverip":28960 --password "$password" - -elif [ "$1" == "duel" ]; then - python3 main.py --host tcp://"$serverip":28961 --password "$password" - -elif [ "$1" == "rcpma" ]; then - python3 main.py --host tcp://"$serverip":28962 --password "$password" - -elif [ "$1" == "iffa" ]; then - python3 main.py --host tcp://"$serverip":28963 --password "$password" - -elif [ "$1" == "ca" ]; then - python3 main.py --host tcp://"$serverip":28964 --password "$password" - -elif [ "$1" == "ctf" ]; then - python3 main.py --host tcp://"$serverip":28965 --password "$password" - -elif [ "$1" == "rcvq3" ]; then - python3 main.py --host tcp://"$serverip":28966 --password "$password" - -elif [ "$1" == "test" ]; then - python3 main.py --host tcp://"$serverip":28967 --password "$password" - -elif [ "$1" == "ft" ]; then - python3 main.py --host tcp://"$serverip":28968 --password "$password" - -elif [ "$1" == "leduel" ]; then - python3 main.py --host tcp://"$serverip":28969 --password "$password" - -else - echo "[ ffa (28960), duel (28961), rcpma (28962), iffa (28963), ca (28964), ctf (28965), rcvq3 (28966), test (28967), ft (28968), leduel (28969) ]" - -fi +cd "$QLPYCON_DIR" +source "$QLPYCON_DIR/venv/bin/activate" +exec python3 main.py "$@" diff --git a/qlpycon.conf.example b/qlpycon.conf.example new file mode 100644 index 0000000..0a1b798 --- /dev/null +++ b/qlpycon.conf.example @@ -0,0 +1,47 @@ +# qlpycon.conf +# Edit this file as needed. +# +# Connect by server name: qlpycon ffa +# Connect directly: qlpycon --host tcp://1.2.3.4:28960 --password secret +# List servers: qlpycon --list + +# === Connection defaults ===> +# Default host if no server name or --host is given +[connection] +host = tcp://127.0.0.1:28960 + +# Password for all servers unless overridden in [server:name] +# Use ${ENV_VAR} to read from environment variable (recommended) +# Or set directly (less secure): +password = ${QLPYCON_PASSWORD:-secret} + +# === Named servers ===> +# Simple entries: name = host:port +# These use the password from [connection] above. +[servers] +# Example: +#ffa = 10.13.12.161:28960 + +# === Per-server overrides ===> +# Use [server:name] to override any setting for a specific server. +# The name must match an entry in [servers] above. +# Example: +#[server:ffa] +#password = ${FFA_PASSWORD:-secret} + +# === Logging ===> +[logging] +# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL +level = WARNING + +# === UI ===> +[ui] +# Number of commands to remember in history +max_history = 10 + +# === Behaviour ===> +[behavior] +# Seconds to confirm quit (press Ctrl-C twice within this window) +quit_timeout = 3.0 +# Seconds before players respawn after death +respawn_delay = 3.0