560 lines
14 KiB
Python
560 lines
14 KiB
Python
#!/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',
|
|
]
|
|
|
|
# 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': '<botname:Sarge|Ranger|Visor|etc> [skill 1-5] [team] [msec delay] [altname]',
|
|
'removebot': '<botname|altname>',
|
|
|
|
# Player management
|
|
'kick': '<player>',
|
|
'kickban': '<player>',
|
|
'ban': '<player>',
|
|
'unban': '<player>',
|
|
'tempban': '<player> <seconds>',
|
|
'tell': '<player> <message>',
|
|
|
|
# Chat & voting
|
|
'say': '<message>',
|
|
'callvote': '<vote type> [args...]',
|
|
'vote': '<yes|no>',
|
|
|
|
# Map commands
|
|
'map': '<mapname>',
|
|
'map_restart': '',
|
|
|
|
# Server
|
|
'rcon': '<command>',
|
|
'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': '<minutes>',
|
|
'fraglimit': '<frags>',
|
|
'capturelimit': '<captures>',
|
|
'roundlimit': '<rounds>',
|
|
'scorelimit': '<score>',
|
|
|
|
# Server settings
|
|
'sv_maxclients': '<1-64>',
|
|
'sv_hostname': '<name>',
|
|
'sv_fps': '<20|30|40|60|125>',
|
|
|
|
# Network
|
|
'net_port': '<port number>',
|
|
|
|
# ZMQ
|
|
'zmq_rcon_enable': '<0|1>',
|
|
'zmq_rcon_port': '<port>',
|
|
'zmq_rcon_password': '<password>',
|
|
'zmq_stats_enable': '<0|1>',
|
|
'zmq_stats_port': '<port>',
|
|
'zmq_stats_password': '<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:
|
|
'<botname> [skill 1-5] [team]' -> ['<botname>', '[skill 1-5]', '[team]']
|
|
"""
|
|
import re
|
|
# Match <arg> 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}")
|