#!/usr/bin/env python3 """ Quake Live console variables (cvars) database Common cvars for autocomplete and fuzzy search """ # Server configuration SERVER_CVARS = [ 'sv_hostname', 'sv_maxclients', 'sv_privateclients', 'sv_fps', 'sv_timeout', 'sv_minrate', 'sv_maxrate', 'sv_floodprotect', 'sv_pure', 'sv_allowdownload', ] # Game rules GAME_CVARS = [ 'g_gametype', 'g_factoryTitle', 'g_motd', 'timelimit', 'fraglimit', 'capturelimit', 'roundlimit', 'scorelimit', 'g_allowKill', 'g_allowSpecVote', 'g_friendlyFire', 'g_teamAutoJoin', 'g_teamForceBalance', 'g_warmupDelay', 'g_inactivity', 'g_quadHog', 'g_training', 'g_instagib', ] # Map and rotation MAP_CVARS = [ 'mapname', 'nextmap', 'g_nextmap', ] # Voting VOTE_CVARS = [ 'g_voteDelay', 'g_voteLimit', 'g_allowVote', ] # Items and weapons ITEM_CVARS = [ 'dmflags', 'weapon_reload_rg', 'weapon_reload_sg', 'weapon_reload_gl', 'weapon_reload_rl', 'weapon_reload_lg', 'weapon_reload_pg', 'weapon_reload_hmg', ] # Network NETWORK_CVARS = [ 'net_port', 'net_ip', 'net_strict', ] # ZMQ ZMQ_CVARS = [ 'zmq_rcon_enable', 'zmq_rcon_ip', 'zmq_rcon_port', 'zmq_rcon_password', 'zmq_stats_enable', 'zmq_stats_ip', 'zmq_stats_port', 'zmq_stats_password', ] # QLX (minqlx) specific QLX_CVARS = [ 'qlx_serverBrandName', 'qlx_owner', 'qlx_redditAuth', ] # Bot cvars BOT_CVARS = [ 'bot_enable', 'bot_nochat', 'bot_minplayers', ] # Common commands (not cvars but useful for autocomplete) COMMANDS = [ 'status', 'map', 'map_restart', 'kick', 'kickban', 'ban', 'unban', 'tempban', 'tell', 'say', 'callvote', 'vote', 'rcon', 'addbot', 'removebot', 'killserver', 'quit', 'team', # Match control 'readyall', 'allready', 'abort', 'pause', 'unpause', 'lock', 'unlock', 'timeout', 'timein', # Player management 'shuffle', 'put', 'mute', 'unmute', 'slap', 'slay', # Server control 'restart', 'endgame', 'nextmap', 'forcemap', # QLX commands 'qlx', 'elo', 'balance', 'teams', 'scores', # Info commands 'serverinfo', 'players', 'maplist', 'configstrings', ] # Bot names for addbot command (complete list) BOT_NAMES = [ 'Anarki', 'Angel', 'Biker', 'Bitterman', 'Bones', 'Cadaver', 'Crash', 'Daemia', 'Doom', 'Gorre', 'Grunt', 'Hossman', 'Hunter', 'Keel', 'Klesk', 'Lucy', 'Major', 'Mynx', 'Orbb', 'Patriot', 'Phobos', 'Ranger', 'Razor', 'Sarge', 'Slash', 'Sorlag', 'Stripe', 'TankJr', 'Uriel', 'Visor', 'Wrack', 'Xaero' ] # Skill levels for bots BOT_SKILL_LEVELS = ['1', '2', '3', '4', '5'] # Team values TEAM_VALUES = ['red', 'blue', 'free', 'spectator', 'r', 'b', 'f', 's'] # Popular competitive Quake Live maps MAP_NAMES = [ 'aerowalk', 'almostlost', 'arenagate', 'asylum', 'battleforged', 'bloodrun', 'brimstoneabbey', 'campgrounds', 'cannedheat', 'citycrossings', 'cure', 'deepinside', 'dismemberment', 'elder', 'eviscerated', 'falloutbunker', 'finnegans', 'furiousheights', 'gospelcrossings', 'grimdungeons', 'hearth', 'hektik', 'lostworld', 'monsoon', 'reflux', 'repent', 'shiningforces', 'sinister', 'spacectf', 'spidercrossings', 'stonekeep', 'terminus', 'tornado', 'toxicity', 'trinity', 'verticalvengeance', 'warehouses', 'whisper' ] # Game type names (string values) GAMETYPE_NAMES = [ 'ffa', 'duel', 'race', 'tdm', 'ca', 'ctf', 'oneflag', 'har', 'ft', 'dom', 'ad', 'rr' ] # Game type numbers (legacy numeric values) GAMETYPE_NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] # Vote types for callvote command VOTE_TYPES = [ 'map', 'map_restart', 'nextmap', 'gametype', 'kick', 'timelimit', 'fraglimit', 'shuffle', 'teamsize', 'cointoss', 'random', 'loadouts', 'ammo', 'timers' ] # Vote values VOTE_VALUES = ['yes', 'no', '1', '2'] # Boolean values (0/1) BOOLEAN_VALUES = ['0', '1'] # sv_fps valid values SV_FPS_VALUES = ['20', '30', '40', '60', '125'] # sv_maxclients common values SV_MAXCLIENTS_VALUES = ['8', '12', '16', '24', '32'] # Common time limits (minutes) TIMELIMIT_VALUES = ['10', '15', '20', '30'] # Common frag limits FRAGLIMIT_VALUES = ['30', '50', '75', '100'] # Common capture limits CAPTURELIMIT_VALUES = ['5', '8', '10'] # Common round limits ROUNDLIMIT_VALUES = ['3', '5', '7', '10'] # Common teamsize values TEAMSIZE_VALUES = ['2', '3', '4', '5', '6'] # Factory types for map command FACTORY_TYPES = ['ffa', 'duel', 'tdm', 'ca', 'ctf', 'iffa', 'ictf', 'ift'] # Command signatures (usage help) COMMAND_SIGNATURES = { # Bot commands 'addbot': ' [skill 1-5] [team] [msec delay] [altname]', 'removebot': '', # Player management 'kick': '', 'kickban': '', 'ban': '', 'unban': '', 'tempban': ' ', 'tell': ' ', # Chat & voting 'say': '', 'callvote': ' [args...]', 'vote': '', # Map commands 'map': '', 'map_restart': '', # Server 'rcon': '', 'killserver': '', 'quit': '', 'status': '', # Game cvars with common values 'g_gametype': '<0=FFA 1=Duel 2=TDM 3=CA 4=CTF 5=OCTF 6=Harv 7=FT 8=Dom>', 'timelimit': '', 'fraglimit': '', 'capturelimit': '', 'roundlimit': '', 'scorelimit': '', # Server settings 'sv_maxclients': '<1-64>', 'sv_hostname': '', 'sv_fps': '<20|30|40|60|125>', # Network 'net_port': '', # ZMQ 'zmq_rcon_enable': '<0|1>', 'zmq_rcon_port': '', 'zmq_rcon_password': '', 'zmq_stats_enable': '<0|1>', 'zmq_stats_port': '', 'zmq_stats_password': '', } # Combine all cvars ALL_CVARS = ( SERVER_CVARS + GAME_CVARS + MAP_CVARS + VOTE_CVARS + ITEM_CVARS + NETWORK_CVARS + ZMQ_CVARS + QLX_CVARS + BOT_CVARS + COMMANDS ) # Sort for binary search ALL_CVARS.sort() # Argument value mappings - maps argument type to list of valid values ARGUMENT_VALUES = { 'botname': BOT_NAMES, 'skill': BOT_SKILL_LEVELS, 'team': TEAM_VALUES, 'mapname': MAP_NAMES, 'gametype': GAMETYPE_NAMES + GAMETYPE_NUMBERS, 'gametype_name': GAMETYPE_NAMES, 'vote_type': VOTE_TYPES, 'vote_value': VOTE_VALUES, 'boolean': BOOLEAN_VALUES, 'sv_fps': SV_FPS_VALUES, 'sv_maxclients': SV_MAXCLIENTS_VALUES, 'timelimit': TIMELIMIT_VALUES, 'fraglimit': FRAGLIMIT_VALUES, 'capturelimit': CAPTURELIMIT_VALUES, 'roundlimit': ROUNDLIMIT_VALUES, 'teamsize': TEAMSIZE_VALUES, 'factory': FACTORY_TYPES, } # Command argument definitions - maps command to list of argument types # Each argument is a dict with: type (value list key), required (bool) COMMAND_ARGUMENTS = { 'addbot': [ {'type': 'botname', 'required': True}, {'type': 'skill', 'required': False}, {'type': 'team', 'required': False}, {'type': 'msec delay number', 'required': False}, # msec delay {'type': 'freetext', 'required': False}, # altname ], 'removebot': [ {'type': 'botname', 'required': True}, ], 'kick': [ {'type': 'player', 'required': True}, ], 'kickban': [ {'type': 'player', 'required': True}, ], 'ban': [ {'type': 'player', 'required': True}, ], 'tempban': [ {'type': 'player', 'required': True}, {'type': 'number', 'required': True}, # seconds ], 'tell': [ {'type': 'player', 'required': True}, {'type': 'freetext', 'required': True}, # message ], 'map': [ {'type': 'mapname', 'required': True}, {'type': 'factory', 'required': False}, ], 'callvote': [ {'type': 'vote_type', 'required': True}, {'type': 'dynamic', 'required': False}, # Depends on vote type ], 'vote': [ {'type': 'vote_value', 'required': True}, ], 'team': [ {'type': 'team', 'required': True}, ], 'g_gametype': [ {'type': 'gametype', 'required': True}, ], 'timelimit': [ {'type': 'timelimit', 'required': True}, ], 'fraglimit': [ {'type': 'fraglimit', 'required': True}, ], 'capturelimit': [ {'type': 'capturelimit', 'required': True}, ], 'roundlimit': [ {'type': 'roundlimit', 'required': True}, ], 'sv_fps': [ {'type': 'sv_fps', 'required': True}, ], 'sv_maxclients': [ {'type': 'sv_maxclients', 'required': True}, ], 'sv_hostname': [ {'type': 'freetext', 'required': True}, ], 'sv_pure': [ {'type': 'boolean', 'required': True}, ], 'zmq_rcon_enable': [ {'type': 'boolean', 'required': True}, ], 'zmq_rcon_port': [ {'type': 'number', 'required': True}, ], 'zmq_rcon_password': [ {'type': 'freetext', 'required': True}, ], 'zmq_stats_enable': [ {'type': 'boolean', 'required': True}, ], 'zmq_stats_port': [ {'type': 'number', 'required': True}, ], 'zmq_stats_password': [ {'type': 'freetext', 'required': True}, ], } def get_argument_suggestions(command, arg_position, current_value, player_list=None): """ Get autocomplete suggestions for a command argument Args: command: Command name (e.g., 'addbot') arg_position: Argument index (0 = first argument after command) current_value: What user has typed so far for this argument player_list: List of player names (for dynamic 'player' type) Returns: List of suggestion strings matching current_value """ # Get command's argument definitions if command not in COMMAND_ARGUMENTS: return [] arg_defs = COMMAND_ARGUMENTS[command] # Check if arg_position is valid if arg_position >= len(arg_defs): return [] arg_def = arg_defs[arg_position] arg_type = arg_def['type'] # Handle special types if arg_type == 'player': # Dynamic: get from player_list parameter if player_list: return fuzzy_match(current_value, player_list, max_results=5) return [] elif arg_type == 'dynamic': # Special case for callvote second argument # Would need first argument to determine suggestions # For now, return empty (could be enhanced later) return [] elif arg_type == 'number': # Show common numeric values return ['50', '100', '150', '200', '250'] elif arg_type == 'freetext': # No suggestions for free text return [] elif arg_type in ARGUMENT_VALUES: # Static list from ARGUMENT_VALUES values = ARGUMENT_VALUES[arg_type] return fuzzy_match(current_value, values, max_results=5) return [] def fuzzy_match(query, candidates, max_results=5): """ Fuzzy match query against candidates Returns list of (match, score) tuples sorted by score Scoring: - Exact match: 1000 - Prefix match: 500 + remaining chars - Substring match: 100 - Contains all chars in order: 50 - Levenshtein-like: based on edit distance """ if not query: # Return first max_results candidates when query is empty return candidates[:max_results] query_lower = query.lower() matches = [] for candidate in candidates: candidate_lower = candidate.lower() score = 0 # Exact match if query_lower == candidate_lower: score = 1000 # Prefix match (best after exact) elif candidate_lower.startswith(query_lower): score = 500 + (100 - len(candidate)) # Prefer shorter matches # Substring match elif query_lower in candidate_lower: # Score higher if match is earlier in string pos = candidate_lower.index(query_lower) score = 100 - pos # Contains all characters in order (fuzzy) else: query_idx = 0 for char in candidate_lower: if query_idx < len(query_lower) and char == query_lower[query_idx]: query_idx += 1 if query_idx == len(query_lower): # All chars found score = 50 if score > 0: matches.append((candidate, score)) # Sort by score (highest first), then alphabetically matches.sort(key=lambda x: (-x[1], x[0])) return [match for match, score in matches[:max_results]] def autocomplete(partial, max_results=5): """ Autocomplete a partial cvar/command Returns list of suggestions """ return fuzzy_match(partial, ALL_CVARS, max_results) def parse_signature(signature): """ Parse command signature into individual arguments Returns list of argument strings Example: ' [skill 1-5] [team]' -> ['', '[skill 1-5]', '[team]'] """ import re # Match or [arg] patterns, including content with spaces pattern = r'(<[^>]+>|\[[^\]]+\])' args = re.findall(pattern, signature) return args def get_signature_with_highlight(command, arg_position): """ Get command signature with current argument highlighted Args: command: Command name (e.g., 'addbot') arg_position: Current argument index (0-based, 0 = first arg after command) Returns: List of (text, is_highlighted) tuples """ if command not in COMMAND_SIGNATURES: return [] signature = COMMAND_SIGNATURES[command] if not signature: return [] args = parse_signature(signature) if not args: return [(signature, False)] # Build list of (arg_text, is_current) tuples result = [] for i, arg in enumerate(args): is_current = (i == arg_position) result.append((arg, is_current)) return result if __name__ == '__main__': # Test autocomplete print("Testing autocomplete:") print(f"Total cvars/commands: {len(ALL_CVARS)}") print() test_queries = ['sv_', 'time', 'zmq', 'qlx', 'map', 'stat', 'g_team'] for query in test_queries: results = autocomplete(query) print(f"'{query}' -> {results}")