modified: .gitignore

modified:   README.md
	new file:   install.sh
	renamed:    config.py -> lib/constants.py
	renamed:    cvars.py -> lib/cvars.py
	renamed:    formatter.py -> lib/formatter.py
	renamed:    network.py -> lib/network.py
	renamed:    parser.py -> lib/parser.py
	renamed:    qlpycon_config.py -> lib/settings.py
	renamed:    state.py -> lib/state.py
	renamed:    ui.py -> lib/ui.py
	modified:   main.py
	modified:   qlpycon.bash
	new file:   qlpycon.conf.example
This commit is contained in:
xbl
2026-06-13 10:21:29 +02:00
parent be6ae8d137
commit 7ca9795a39
14 changed files with 476 additions and 174 deletions

4
.gitignore vendored
View File

@ -1,8 +1,8 @@
__pycache__/ __pycache__/
venv3/ venv3/
*test*
*.json *.json
*.log *.log
cvarlist.txt cvarlist.txt
curztest.py qlpycon.conf
rconpw.txt rconpw.txt
CLAUDE*.md

111
README.md
View File

@ -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. Terminal client for monitoring and controlling Quake Live servers via ZMQ RCON.
## 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)
## Installation ## Installation
```bash ```bash
pip install pyzmq curl -sSL https://6bit.ch/qlpycon/install.sh | bash
python main.py --host tcp://SERVER_IP:PORT --password RCON_PASSWORD ```
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 ## Usage
```bash ```bash
# Basic qlpycon ffa # connect by name
python main.py --host tcp://SERVER_IP:PORT --password RCON_PASSWORD qlpycon --host tcp://10.13.12.93:28960 --password secret # connect directly
qlpycon --list # list configured servers
# 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
``` ```
**Options:** **Options:**
- `--host URI` - ZMQ RCON endpoint (default: tcp://127.0.0.1:27961) - `--host URI` ZMQ RCON endpoint
- `--password PASS` - RCON password (required) - `--password PASS` RCON password (or set `QLPYCON_PASSWORD` env var)
- `-v` / `-vv` - Verbose (INFO) or debug (DEBUG) logging - `--list` — list configured servers and exit
- `--json FILE` - Log all events as JSON - `-v` / `-vv` — verbose (INFO) or debug (DEBUG) logging
- `--unknown-log FILE` - Log unparsed events (default: unknown_events.log) - `--json FILE` — log all JSON events to file
- `--unknown-log FILE` — log unparsed events (default: unknown_events.log)
**Configuration File (Optional):** ## Features
Create `~/.qlpycon.conf` or `./qlpycon.conf`:
```ini
[connection]
host = tcp://SERVER_IP:PORT
password = your_password
[logging] - Real-time kill/death/medal/team switch events
level = INFO - 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:** See [AUTOCOMPLETE.md](AUTOCOMPLETE.md) for input details.
- **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 <botname> [skill 1-5] [team]`)
See [AUTOCOMPLETE.md](AUTOCOMPLETE.md) for details.
## Architecture ## Architecture
``` ```
main.py - Main loop, argument parsing, signal handling main.py — entry point, arg parsing, signal handling
config.py - Constants (weapons, teams, colors, limits) qlpycon.conf — user configuration
state.py - Game state (ServerInfo, PlayerTracker, EventDeduplicator) lib/
network.py - ZMQ connections (RCON DEALER, Stats SUB sockets) constants.py — weapons, teams, colors, limits
parser.py - JSON event parsing (deaths, medals, switches, stats) settings.py — config file loader
formatter.py - Message formatting, color codes, team prefixes state.py — game state (server info, players, teams)
ui.py - Curses interface (3-panel: info, output, input) 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 ## License
WTFPL WTFPL

218
install.sh Executable file
View File

@ -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 </dev/tty
INSTALL_DIR="${USER_DIR:-$DEFAULT_DIR}"
INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}"
download_and_extract
fi
setup_venv
setup_config
write_launcher
check_path
echo ""
success "qlpycon installed"
echo ""
echo " Edit your config: ${INSTALL_DIR}/qlpycon.conf"
echo " Run: qlpycon --list"
echo " qlpycon ffa"
echo " qlpycon --host tcp://1.2.3.4:28960 --password secret"
echo ""
}
main "$@"

View File

@ -6,7 +6,7 @@ Handles Quake color codes and team prefixes
import re import re
import time import time
from config import TEAM_COLORS, COLOR_CODE_PATTERN, SPECIAL_CHAR from .constants import TEAM_COLORS, COLOR_CODE_PATTERN, SPECIAL_CHAR
def strip_color_codes(text): def strip_color_codes(text):
@ -169,7 +169,7 @@ def format_powerup_message(message, player_tracker):
Format powerup pickup and carrier kill messages Format powerup pickup and carrier kill messages
Returns formatted message or None if not a powerup message Returns formatted message or None if not a powerup message
""" """
from config import POWERUP_COLORS from .constants import POWERUP_COLORS
import time import time
if message.startswith("broadcast:"): if message.startswith("broadcast:"):

View File

@ -7,8 +7,8 @@ Parses events from Quake Live stats stream
import json import json
import logging import logging
import time import time
from config import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES from .constants import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES
from formatter import get_team_prefix, strip_color_codes from .formatter import get_team_prefix, strip_color_codes
logger = logging.getLogger('parser') logger = logging.getLogger('parser')

View File

@ -82,11 +82,50 @@ class ConfigLoader:
if not password: if not password:
return None return None
# Support environment variable substitution: ${VAR_NAME} return self._resolve_password(password)
if password.startswith('${') and password.endswith('}'):
env_var = password[2:-1]
return os.environ.get(env_var)
def get_servers(self):
"""Return dict of name -> 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 return password
def get_log_level(self): def get_log_level(self):
@ -104,29 +143,52 @@ class ConfigLoader:
def create_example_config(): def create_example_config():
"""Create an example configuration file""" """Create an example configuration file"""
config_content = """# QLPyCon Configuration File config_content = """# qlpycon.conf
# Place this file as ~/.qlpycon.conf or ./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] [connection]
# Server connection settings host = tcp://127.0.0.1:28960
host = tcp://10.13.12.93:28969
# Use ${ENV_VAR} to read from environment
password = ${QLPYCON_PASSWORD}
# 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] [logging]
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL # Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
level = INFO level = WARNING
# === UI ===>
[ui] [ui]
# Max command history entries # Number of commands to remember in history
max_history = 10 max_history = 10
# Color scheme (future feature)
color_scheme = quake
# === Behaviour ===>
[behavior] [behavior]
# Quit confirmation timeout (seconds) # Seconds to confirm quit (press Ctrl-C twice within this window)
quit_timeout = 3.0 quit_timeout = 3.0
# Player respawn delay (seconds) # Seconds before players respawn after death
respawn_delay = 3.0 respawn_delay = 3.0
""" """

View File

@ -5,8 +5,8 @@ Tracks server info, players, and teams
""" """
import logging import logging
from config import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS from .constants import TEAM_MODES, TEAM_MAP, MAX_RECENT_EVENTS
from formatter import strip_color_codes from .formatter import strip_color_codes
logger = logging.getLogger('state') logger = logging.getLogger('state')

View File

@ -10,8 +10,8 @@ import threading
import queue import queue
import logging import logging
import time 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 .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 from .cvars import autocomplete, COMMAND_SIGNATURES, get_signature_with_highlight, get_argument_suggestions, COMMAND_ARGUMENTS
logger = logging.getLogger('ui') 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 '' 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 '' 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) red_clean = strip_color_codes(red)
blue_clean = strip_color_codes(blue) 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 '' 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 '' 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) col1_clean = strip_color_codes(col1)
col2_clean = strip_color_codes(col2) col2_clean = strip_color_codes(col2)

87
main.py
View File

@ -16,13 +16,13 @@ import sys
import os import os
import threading import threading
from config import VERSION, DEFAULT_HOST, POLL_TIMEOUT, QUIT_CONFIRM_TIMEOUT, RESPAWN_DELAY, MAX_COMMAND_HISTORY from lib.constants import VERSION, DEFAULT_HOST, POLL_TIMEOUT, QUIT_CONFIRM_TIMEOUT, RESPAWN_DELAY, MAX_COMMAND_HISTORY
from state import GameState from lib.state import GameState
from network import RconConnection, StatsConnection from lib.network import RconConnection, StatsConnection
from parser import EventParser from lib.parser import EventParser
from formatter import format_message, format_chat_message, format_powerup_message, strip_color_codes from lib.formatter import format_message, format_chat_message, format_powerup_message, strip_color_codes
from ui import UIManager from lib.ui import UIManager
from qlpycon_config import ConfigLoader from lib.settings import ConfigLoader
# Pre-compiled regex patterns # Pre-compiled regex patterns
CVAR_RESPONSE_PATTERN = re.compile(r'"([^"]+)"\s+is:"([^"]*)"') CVAR_RESPONSE_PATTERN = re.compile(r'"([^"]+)"\s+is:"([^"]*)"')
@ -299,32 +299,12 @@ def parse_player_events(message, game_state, ui):
return False return False
def main_loop(screen): def main_loop(screen, args):
"""Main application loop""" """Main application loop"""
# Setup signal handler for Ctrl+C with confirmation # Setup signal handler for Ctrl+C with confirmation
signal.signal(signal.SIGINT, signal_handler) 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 # Set logging level
if args.verbose == 0: if args.verbose == 0:
logger.setLevel(logging.WARNING) 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") ui.print_message(f"zmq python bindings {zmq.__version__}, libzmq version {zmq.zmq_version()}\n")
# Initialize network connections # 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 = RconConnection(args.host, args.password, args.identity)
rcon.connect() rcon.connect()
@ -546,4 +527,52 @@ def main_loop(screen):
logger.info("Shutdown complete") logger.info("Shutdown complete")
if __name__ == '__main__': 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)

View File

@ -1,66 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# # qlpycon launcher — generated by install.sh
# Helper script to connect to different Quake Live servers QLPYCON_DIR="/home/xbl/gitz/qlpycon"
# Required: QLPYCON_PASSWORD
# Optional: QLPYCON_WORKDIR (default: /home/marc/git/qlpycon.git)
# QLPYCON_SERVERIP (default: 10.13.12.93)
# Required if [[ ! -d "$QLPYCON_DIR" ]]; then
password="${QLPYCON_PASSWORD:-}" echo "Error: qlpycon directory not found: $QLPYCON_DIR"
workdir="${QLPYCON_WORKDIR:-/home/xbl/gitz/qlpycon}" echo "Re-run install.sh or cry :("
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'"
exit 1 exit 1
fi fi
if [ ! -d "$workdir" ]; then cd "$QLPYCON_DIR"
echo "Error: Working directory does not exist: $workdir" source "$QLPYCON_DIR/venv/bin/activate"
exit 1 exec python3 main.py "$@"
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

47
qlpycon.conf.example Normal file
View File

@ -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