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:
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,8 +1,8 @@
|
||||
__pycache__/
|
||||
venv3/
|
||||
*test*
|
||||
*.json
|
||||
*.log
|
||||
cvarlist.txt
|
||||
curztest.py
|
||||
qlpycon.conf
|
||||
rconpw.txt
|
||||
CLAUDE*.md
|
||||
|
||||
111
README.md
111
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 <botname> [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
|
||||
|
||||
218
install.sh
Executable file
218
install.sh
Executable 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 "$@"
|
||||
@ -6,7 +6,7 @@ Handles Quake color codes and team prefixes
|
||||
|
||||
import re
|
||||
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):
|
||||
@ -169,7 +169,7 @@ def format_powerup_message(message, player_tracker):
|
||||
Format powerup pickup and carrier kill messages
|
||||
Returns formatted message or None if not a powerup message
|
||||
"""
|
||||
from config import POWERUP_COLORS
|
||||
from .constants import POWERUP_COLORS
|
||||
import time
|
||||
|
||||
if message.startswith("broadcast:"):
|
||||
@ -7,8 +7,8 @@ Parses events from Quake Live stats stream
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from config import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES
|
||||
from formatter import get_team_prefix, strip_color_codes
|
||||
from .constants import WEAPON_NAMES, WEAPON_KILL_NAMES, DEATH_MESSAGES
|
||||
from .formatter import get_team_prefix, strip_color_codes
|
||||
|
||||
logger = logging.getLogger('parser')
|
||||
|
||||
@ -82,11 +82,50 @@ class ConfigLoader:
|
||||
if not password:
|
||||
return None
|
||||
|
||||
# Support environment variable substitution: ${VAR_NAME}
|
||||
if password.startswith('${') and password.endswith('}'):
|
||||
env_var = password[2:-1]
|
||||
return os.environ.get(env_var)
|
||||
return self._resolve_password(password)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)
|
||||
|
||||
87
main.py
87
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)
|
||||
|
||||
69
qlpycon.bash
69
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 "$@"
|
||||
|
||||
47
qlpycon.conf.example
Normal file
47
qlpycon.conf.example
Normal 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
|
||||
Reference in New Issue
Block a user