def handle_new_game(is_restart): # This is called early in the launch process, so it's a good place to initialize # minqlx stuff that needs QLDS to be initialized. global _first_game if _first_game: minqlx.late_init() _first_game = False # A good place to warn the owner if ZMQ stats are disabled. global _zmq_warning_issued if not bool(int(minqlx.get_cvar( "zmq_stats_enable"))) and not _zmq_warning_issued: logger = minqlx.get_logger() logger.warning( "Some events will not work because ZMQ stats is not enabled. " "Launch the server with \"zmq_stats_enable 1\"") _zmq_warning_issued = True minqlx.set_map_subtitles() if not is_restart: try: minqlx.EVENT_DISPATCHERS["map"].dispatch( minqlx.get_cvar("mapname"), minqlx.get_cvar("g_factory")) except: minqlx.log_exception() return True try: minqlx.EVENT_DISPATCHERS["new_game"].dispatch() except: minqlx.log_exception() return True
def __init__(self): self.done: bool if not bool(int(minqlx.get_cvar("zmq_stats_enable"))): self.done = True return stats: Optional[str] = minqlx.get_cvar("zmq_stats_ip") port: Optional[str] = minqlx.get_cvar("zmq_stats_port") if not port: port = minqlx.get_cvar("net_port") host = "127.0.0.1" if not stats else stats self.address: str = f"tcp://{host}:{port}" self.password: Optional[str] = minqlx.get_cvar("zmq_stats_password") # Initialize socket, connect, and subscribe. self.context: zmq.Context = zmq.Context() self.socket: zmq.Socket = self.context.socket(zmq.SUB) if self.password: self.socket.plain_username = b"stats" self.socket.plain_password = self.password.encode() self.socket.zap_domain = b"stats" self.socket.connect(self.address) self.socket.setsockopt_string(zmq.SUBSCRIBE, "") self.done = False self._in_progress: bool = False
def handle_new_game(is_restart): # This is called early in the launch process, so it's a good place to initialize # minqlx stuff that needs QLDS to be initialized. global _first_game if _first_game: minqlx.late_init() _first_game = False # A good place to warn the owner if ZMQ stats are disabled. global _zmq_warning_issued if not bool(int(minqlx.get_cvar("zmq_stats_enable"))) and not _zmq_warning_issued: logger = minqlx.get_logger() logger.warning("Some events will not work because ZMQ stats is not enabled. " "Launch the server with \"zmq_stats_enable 1\"") _zmq_warning_issued = True minqlx.set_map_subtitles() if not is_restart: try: minqlx.EVENT_DISPATCHERS["map"].dispatch( minqlx.get_cvar("mapname"), minqlx.get_cvar("g_factory")) except: minqlx.log_exception() return True try: minqlx.EVENT_DISPATCHERS["new_game"].dispatch() except: minqlx.log_exception() return True
def __init__(self): if not bool(int(minqlx.get_cvar("zmq_stats_enable"))): self.done = True return stats = minqlx.get_cvar("zmq_stats_ip") port = minqlx.get_cvar("zmq_stats_port") if not port: port = minqlx.get_cvar("net_port") self.address = "tcp://{}:{}".format( "127.0.0.1" if not stats else stats, port) self.password = minqlx.get_cvar("zmq_stats_password") # Initialize socket, connect, and subscribe. self.context = zmq.Context() self.socket = self.context.socket(zmq.SUB) if self.password: self.socket.plain_username = b"stats" self.socket.plain_password = self.password.encode() self.socket.zap_domain = b"stats" self.socket.connect(self.address) self.socket.setsockopt_string(zmq.SUBSCRIBE, "") self.done = False self._in_progress = False
def is_eligible_player(self, player, is_client_cmd): """Check if a player has the rights to execute the command.""" # Check if config overrides permission. perm = self.permission client_cmd_perm = self.client_cmd_perm if is_client_cmd: cvar_client_cmd = minqlx.get_cvar("qlx_ccmd_perm_" + self.name[0]) if cvar_client_cmd: client_cmd_perm = int(cvar_client_cmd) else: cvar = minqlx.get_cvar("qlx_perm_" + self.name[0]) if cvar: perm = int(cvar) if (player.steam_id == minqlx.owner() or (not is_client_cmd and perm == 0) or (is_client_cmd and client_cmd_perm == 0)): return True player_perm = self.plugin.db.get_permission(player) if is_client_cmd: return player_perm >= client_cmd_perm else: return player_perm >= perm
def connect(self, host=None, database=0, unix_socket=False, password=None): """Returns a connection to a Redis database. If *host* is None, it will fall back to the settings in the config and ignore the rest of the arguments. It will also share the connection across any plugins using the default configuration. Passing *host* will make it connect to a specific database that is not shared at all. Subsequent calls to this will return the connection initialized the first call unless it has been closed. :param host: The host name. If no port is specified, it will use 6379. Ex.: ``localhost:1234``. :type host: str :param database: The database number that should be used. :type database: int :param unix_socket: Whether or not *host* should be interpreted as a unix socket path. :type unix_socket: bool :raises: RuntimeError """ if not host and not self._conn: # Resort to default settings in config? if not Redis._conn: cvar_host = minqlx.get_cvar("qlx_redisAddress") cvar_db = int(minqlx.get_cvar("qlx_redisDatabase")) cvar_unixsocket = bool(int(minqlx.get_cvar("qlx_redisUnixSocket"))) Redis._pass = minqlx.get_cvar("qlx_redisPassword") if cvar_unixsocket: Redis._conn = redis.StrictRedis(unix_socket_path=cvar_host, db=cvar_db, password=Redis._pass, decode_responses=True) else: split_host = cvar_host.split(":") if len(split_host) > 1: port = int(split_host[1]) else: port = 6379 # Default port. Redis._pool = redis.ConnectionPool(host=split_host[0], port=port, db=cvar_db, password=Redis._pass, decode_responses=True) Redis._conn = redis.StrictRedis(connection_pool=Redis._pool, decode_responses=True) # TODO: Why does self._conn get set when doing Redis._conn? self._conn = None return Redis._conn elif not self._conn: split_host = host.split(":") if len(split_host) > 1: port = int(split_host[1]) else: port = 6379 # Default port. if unix_socket: self._conn = redis.StrictRedis(unix_socket_path=host, db=database, password=password, decode_responses=True) else: self._pool = redis.ConnectionPool(host=split_host[0], port=port, db=database, password=password, decode_responses=True) self._conn = redis.StrictRedis(connection_pool=self._pool, decode_responses=True) return self._conn
def start_stats_listener(): if bool(int(minqlx.get_cvar("zmq_stats_enable"))): global _stats _stats = minqlx.StatsListener() logger.info("Stats listener started on {}.".format(_stats.address)) # Start polling. Not blocking due to decorator magic. Aw yeah. _stats.keep_receiving()
def get_maxplayers(self): maxplayers = int(self.game.teamsize) if self.game.type_short in TEAM_BASED_GAMETYPES: maxplayers = maxplayers * 2 if maxplayers == 0: maxplayers = minqlx.get_cvar("sv_maxClients", int) return maxplayers
def add_hook(self, plugin, handler, priority=minqlx.PRI_NORMAL): """Hook the event, making the handler get called with relevant arguments whenever the event is takes place. :param plugin: The plugin that's hooking the event. :type plugin: minqlx.Plugin :param handler: The handler to be called when the event takes place. :type handler: callable :param priority: The priority of the hook. Determines the order the handlers are called in. :type priority: minqlx.PRI_LOWEST, minqlx.PRI_LOW, minqlx.PRI_NORMAL, minqlx.PRI_HIGH or minqlx.PRI_HIGHEST :raises: ValueError """ if not (minqlx.PRI_HIGHEST <= priority <= minqlx.PRI_LOWEST): raise ValueError("'{}' is an invalid priority level.".format(priority)) if self.need_zmq_stats_enabled and not bool(int(minqlx.get_cvar("zmq_stats_enable"))): raise AssertionError("{} hook requires zmq_stats_enabled cvar to have nonzero value".format(self.name)) if plugin not in self.plugins: # Initialize tuple. self.plugins[plugin] = ([], [], [], [], []) # 5 priority levels. else: # Check if we've already registered this handler. for i in range(len(self.plugins[plugin])): for hook in self.plugins[plugin][i]: if handler == hook: raise ValueError("The event has already been hooked with the same handler and priority.") self.plugins[plugin][priority].append(handler)
def __init__(self): self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_LOWEST) self.add_hook("player_disconnect", self.handle_player_disconnect, priority=minqlx.PRI_LOWEST) self.add_hook("chat", self.handle_chat, priority=minqlx.PRI_LOWEST) self.add_hook("command", self.handle_command, priority=minqlx.PRI_LOWEST) self.set_cvar_once("qlx_chatlogs", "3") self.set_cvar_once("qlx_chatlogsSize", str(3 * 10 ** 6)) # 3 MB self.chatlog = logging.Logger(__name__) file_dir = os.path.join(minqlx.get_cvar("fs_homepath"), "chatlogs") if not os.path.isdir(file_dir): os.makedirs(file_dir) file_path = os.path.join(file_dir, "chat.log") maxlogs = minqlx.Plugin.get_cvar("qlx_chatlogs", int) maxlogsize = minqlx.Plugin.get_cvar("qlx_chatlogsSize", int) file_fmt = logging.Formatter("[%(asctime)s] %(message)s", "%Y-%m-%d %H:%M:%S") file_handler = RotatingFileHandler(file_path, encoding="utf-8", maxBytes=maxlogsize, backupCount=maxlogs) file_handler.setFormatter(file_fmt) self.chatlog.addHandler(file_handler) self.chatlog.info( "============================= Logger started @ {} =============================".format( datetime.datetime.now() ) )
def get_cvar(cls, name, return_type=str): """Gets the value of a cvar as a string. :param name: The name of the cvar. :type name: str :param return_type: The type the cvar should be returned in. Supported types: str, int, float, bool, list, tuple """ res = minqlx.get_cvar(name) if return_type == str: return res elif return_type == int: return int(res) elif return_type == float: return float(res) elif return_type == bool: return bool(int(res)) elif return_type == list: return [s.strip() for s in res.split(",")] elif return_type == set: return {s.strip() for s in res.split(",")} elif return_type == tuple: return tuple([s.strip() for s in res.split(",")]) else: raise ValueError("Invalid return type: {}".format(return_type))
def load_plugin(plugin): logger = get_logger(None) logger.info("Loading plugin '{}'...".format(plugin)) plugins = minqlx.Plugin._loaded_plugins plugins_path = os.path.abspath(minqlx.get_cvar("qlx_pluginsPath")) plugins_dir = os.path.basename(plugins_path) if not os.path.isfile(os.path.join(plugins_path, plugin + ".py")): raise PluginLoadError("No such plugin exists.") elif plugin in plugins: return reload_plugin(plugin) try: module = importlib.import_module("{}.{}".format(plugins_dir, plugin)) # We add the module regardless of whether it fails or not, otherwise we can't reload later. global _modules _modules[plugin] = module if not hasattr(module, plugin): raise (PluginLoadError( "The plugin needs to have a class with the exact name as the file, minus the .py." )) plugin_class = getattr(module, plugin) if issubclass(plugin_class, minqlx.Plugin): plugins[plugin] = plugin_class() else: raise (PluginLoadError( "Attempted to load a plugin that is not a subclass of 'minqlx.Plugin'." )) except: log_exception(plugin) raise
def handle_new_game(): # This is called early in the launch process, so it's a good place to warn # the owner if ZMQ stats are disabled. if not bool(int(minqlx.get_cvar("zmq_stats_enable"))) and not _zmq_warning_issued: global _zmq_warning_issued logger = minqlx.get_logger() logger.warning( "Some events will not work because ZMQ stats is not enabled. " 'Launch the server with "zmq_stats_enable 1"' ) _zmq_warning_issued = True try: minqlx.EVENT_DISPATCHERS["map"].dispatch(minqlx.get_cvar("mapname"), minqlx.get_cvar("g_factory")) except: minqlx.log_exception() return True
def process_frame(self): self.frame_counter += 1 if self.frame_counter == ( int(self.get_cvar("qlx_pingSpecSecondsBetweenChecks")) * int(minqlx.get_cvar("sv_fps"))): self.frame_counter = 0 self.check_ping()
def _configure_logger(): logger = logging.getLogger("minqlx") logger.setLevel(logging.DEBUG) # File file_path = os.path.join(minqlx.get_cvar("fs_homepath"), "minqlx.log") maxlogs = minqlx.Plugin.get_cvar("qlx_logs", int) maxlogsize = minqlx.Plugin.get_cvar("qlx_logsSize", int) file_fmt = logging.Formatter( "(%(asctime)s) [%(levelname)s @ %(name)s.%(funcName)s] %(message)s", "%H:%M:%S") file_handler = RotatingFileHandler(file_path, encoding="utf-8", maxBytes=maxlogsize, backupCount=maxlogs) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_fmt) logger.addHandler(file_handler) logger.info( "============================= minqlx run @ {} =============================" .format(datetime.datetime.now())) # Console console_fmt = logging.Formatter( "[%(name)s.%(funcName)s] %(levelname)s: %(message)s", "%H:%M:%S") console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_fmt) logger.addHandler(console_handler)
def load_preset_plugins() -> None: plugins_temp = [] plugins_cvar = minqlx.Plugin.get_cvar("qlx_plugins", list) if plugins_cvar is None: return for p in plugins_cvar: if p == "DEFAULT": plugins_temp += list(DEFAULT_PLUGINS) else: plugins_temp.append(p) plugins = [] for p in plugins_temp: if p not in plugins: plugins.append(p) plugins_path_cvar = minqlx.get_cvar("qlx_pluginsPath") if plugins_path_cvar is None: raise PluginLoadError("cvar qlx_pluginsPath misconfigured") plugins_path = os.path.abspath(plugins_path_cvar) plugins_dir = os.path.basename(plugins_path) if os.path.isdir(plugins_path): plugins = [p for p in plugins if f"{plugins_dir}.{p}"] for p in plugins: load_plugin(p) else: raise PluginLoadError( f"Cannot find the plugins directory '{os.path.abspath(plugins_path)}'." )
def load_plugin(plugin): logger = get_logger(None) logger.info("Loading plugin '{}'...".format(plugin)) plugins = minqlx.Plugin._loaded_plugins plugins_path = os.path.abspath(minqlx.get_cvar("qlx_pluginsPath")) plugins_dir = os.path.basename(plugins_path) if not os.path.isfile(os.path.join(plugins_path, plugin + ".py")): raise PluginLoadError("No such plugin exists.") elif plugin in plugins: return reload_plugin(plugin) try: module = importlib.import_module("{}.{}".format(plugins_dir, plugin)) # We add the module regardless of whether it fails or not, otherwise we can't reload later. global _modules _modules[plugin] = module if not hasattr(module, plugin): raise(PluginLoadError("The plugin needs to have a class with the exact name as the file, minus the .py.")) plugin_class = getattr(module, plugin) if issubclass(plugin_class, minqlx.Plugin): plugins[plugin] = plugin_class() else: raise(PluginLoadError("Attempted to load a plugin that is not a subclass of 'minqlx.Plugin'.")) except: log_exception(plugin) raise
def get_cvar(cls, name: str, return_type: Type[Union[str, bool, int, float, list, set, tuple]] = str) \ -> Optional[Union[str, bool, int, float, List[str], Set[str], Tuple[str, ...]]]: """Gets the value of a cvar as a string. :param: name: The name of the cvar. :type: name: str :param: return_type: The type the cvar should be returned in. Supported types: str, int, float, bool, list, tuple """ res = minqlx.get_cvar(name) if return_type == str: return res if return_type == int: return int(res) if res else None if return_type == float: return float(res) if res else None if return_type == bool: return bool(int(res)) if res else False if return_type == list: return [s.strip() for s in res.split(",")] if res else [] if return_type == set: return {s.strip() for s in res.split(",")} if res else set() if return_type == tuple: return ([s.strip() for s in res.split(",")]) if res else () raise ValueError(f"Invalid return type: {return_type}")
def get_maxplayers(self): maxplayers = int(self.game.teamsize) if self.game.type_short in TEAM_BASED_GAMETYPES: maxplayers = maxplayers * 2 if maxplayers == 0: maxplayers = minqlx.get_cvar("sv_maxClients") return maxplayers
def is_eligible_name(self, name): if self.prefix: prefix = minqlx.get_cvar("qlx_commandPrefix") if not name.startswith(prefix): return False name = name[len(prefix):] return name.lower() in self.name
def handle_new_game(): # This is called early in the launch process, so it's a good place to warn # the owner if ZMQ stats are disabled. if not bool(int( minqlx.get_cvar("zmq_stats_enable"))) and not _zmq_warning_issued: global _zmq_warning_issued logger = minqlx.get_logger() logger.warning( "Some events will not work because ZMQ stats is not enabled. " "Launch the server with \"zmq_stats_enable 1\"") _zmq_warning_issued = True try: minqlx.EVENT_DISPATCHERS["map"].dispatch(minqlx.get_cvar("mapname"), minqlx.get_cvar("g_factory")) except: minqlx.log_exception() return True
def owner(): """Returns the SteamID64 of the owner. This is set in the config.""" try: sid = int(minqlx.get_cvar("qlx_owner")) if sid == -1: raise RuntimeError return sid except: logger = minqlx.get_logger() logger.error("Failed to parse the Owner Steam ID. Make sure it's in SteamID64 format.")
def set_cvar_limit_once(name: str, value: Union[int, float], minimum: Union[int, float], maximum: Union[int, float], flags: int = 0) -> bool: if minqlx.get_cvar(name) is None: minqlx.set_cvar_limit(name, value, minimum, maximum, flags) return True return False
def late_init() -> None: """Initialization that needs to be called after QLDS has finished its own initialization. """ minqlx.initialize_cvars() # Set the default database plugins should use. # TODO: Make Plugin.database setting generic. database_cvar = minqlx.get_cvar("qlx_database") if database_cvar is not None and database_cvar.lower() == "redis": minqlx.Plugin.database = minqlx.database.Redis # Get the plugins path and set minqlx.__plugins_version__. plugins_path_cvar = minqlx.get_cvar("qlx_pluginsPath") if plugins_path_cvar is not None: plugins_path = os.path.abspath(plugins_path_cvar) set_plugins_version(plugins_path) # Add the plugins path to PATH so that we can load plugins later. sys.path.append(os.path.dirname(plugins_path)) # Initialize the logger now that we have fs_basepath. _configure_logger() logger = get_logger() # Set our own exception handler so that we can log them if unhandled. sys.excepthook = handle_exception if sys.version_info >= (3, 8): threading.excepthook = threading_excepthook logger.info("Loading preset plugins...") load_preset_plugins() stats_enable_cvar = minqlx.get_cvar("zmq_stats_enable") if stats_enable_cvar is not None and bool(int(stats_enable_cvar)): global _stats # pylint: disable=global-statement _stats = minqlx.StatsListener() logger.info("Stats listener started on %s.", _stats.address) # Start polling. Not blocking due to decorator magic. Aw yeah. _stats.keep_receiving() logger.info("We're good to go!")
def __init__(self): if not bool(int(minqlx.get_cvar("zmq_stats_enable"))): self.done = True return stats = minqlx.get_cvar("zmq_stats_ip") port = minqlx.get_cvar("zmq_stats_port") if not port: port = minqlx.get_cvar("net_port") self.address = "tcp://{}:{}".format("127.0.0.1" if not stats else stats, port) self.password = minqlx.get_cvar("zmq_stats_password") # Initialize socket, connect, and subscribe. self.context = zmq.Context() self.socket = self.context.socket(zmq.SUB) self.socket.plain_username = b"stats" self.socket.plain_password = self.password.encode() self.socket.zap_domain = b"stats" self.socket.connect(self.address) self.socket.setsockopt_string(zmq.SUBSCRIBE, "") self.done = False self._in_progress = False
def late_init(): """Initialization that needs to be called after QLDS has finished its own initialization. """ minqlx.initialize_cvars() # Set the default database plugins should use. # TODO: Make Plugin.database setting generic. if minqlx.get_cvar("qlx_database").lower() == "redis": minqlx.Plugin.database = minqlx.database.Redis # Get the plugins path and set minqlx.__plugins_version__. plugins_path = os.path.abspath(minqlx.get_cvar("qlx_pluginsPath")) set_plugins_version(plugins_path) # Initialize the logger now that we have fs_basepath. _configure_logger() logger = get_logger() # Set our own exception handler so that we can log them if unhandled. sys.excepthook = handle_exception # Add the plugins path to PATH so that we can load plugins later. sys.path.append(os.path.dirname(plugins_path)) logger.info("Loading preset plugins...") load_preset_plugins() if bool(int(minqlx.get_cvar("zmq_stats_enable"))): global _stats _stats = minqlx.StatsListener() logger.info("Stats listener started on {}.".format(_stats.address)) # Start polling. Not blocking due to decorator magic. Aw yeah. _stats.keep_receiving() logger.info("We're good to go!")
def print_instructions(self, player): player.tell("^4Wipeout is a modified Clan Arena gametype with respawns played in Quake Live.\n" "A team wins by having all the players on the opposing team dead at the same time.\n" "When a player dies they spectate for a certain time period. " "The time period starts at 5 seconds but is increased by {1} seconds" " for each death on the player's team.\nThe first team to win {2} rounds wins the match.\n" "When round starts you are given 2 power up holdables. Medkit and either\n" "Invulnerability-Shield/Teleport/Flight/Kamikazee\nwith a {3} percent chance of getting kamakazi.\n" "^1To use them you need to have 2 keys bound:\n" "^61) ^1bind <key> \"+button2\" ^3This is the use bind\n" "^62) ^1bind <key> \"say {0}power\" ^3Item swap (medkit and powerup) bind\n" "Quotes used in the BIND commands need to be ^6double-quotes^3.\n" "If the server is blocking saying {0}power in chat, it will not spam the server." .format(minqlx.get_cvar("qlx_commandPrefix"), self.add_seconds, self.get_cvar("qlx_wipeoutRounds"), self.get_cvar("qlx_wipeoutKamakazi")))
def load_preset_plugins(): plugins = minqlx.Plugin.get_cvar("qlx_plugins", set) if "DEFAULT" in plugins: plugins.remove("DEFAULT") plugins.update(DEFAULT_PLUGINS) plugins_path = os.path.abspath(minqlx.get_cvar("qlx_pluginsPath")) plugins_dir = os.path.basename(plugins_path) if os.path.isdir(plugins_path): plugins = [p for p in plugins if "{}.{}".format(plugins_dir, p)] for p in plugins: load_plugin(p) else: raise(PluginLoadError("Cannot find the plugins directory '{}'." .format(os.path.abspath(plugins_path))))
def owner() -> Optional[int]: # pylint: disable=inconsistent-return-statements """Returns the SteamID64 of the owner. This is set in the config.""" # noinspection PyBroadException try: owner_cvar = minqlx.get_cvar("qlx_owner") if owner_cvar is None: raise RuntimeError sid = int(owner_cvar) if sid == -1: raise RuntimeError return sid except: # pylint: disable=bare-except logger = minqlx.get_logger() logger.error( "Failed to parse the Owner Steam ID. Make sure it's in SteamID64 format." ) return None
def load_preset_plugins(): plugins = minqlx.Plugin.get_cvar("qlx_plugins", set) if "DEFAULT" in plugins: plugins.remove("DEFAULT") plugins.update(DEFAULT_PLUGINS) plugins_path = os.path.abspath(minqlx.get_cvar("qlx_pluginsPath")) plugins_dir = os.path.basename(plugins_path) if os.path.isdir(plugins_path): plugins = [p for p in plugins if "{}.{}".format(plugins_dir, p)] for p in plugins: load_plugin(p) else: raise (PluginLoadError( "Cannot find the plugins directory '{}'.".format( os.path.abspath(plugins_path))))
def __init__(self): self.add_hook("new_game", self.handle_new_game) self.add_hook("game_end", self.handle_game_end) self.add_hook("player_loaded", self.handle_player_loaded) self.add_hook("player_disconnect", self.handle_player_disconnect) self.add_hook("team_switch", self.handle_team_switch) self.add_hook("team_switch_attempt", self.handle_team_switch_attempt) self.add_hook("set_configstring", self.handle_configstring, priority=minqlx.PRI_HIGH) self.add_hook("client_command", self.handle_client_command) self.add_hook("vote_ended", self.handle_vote_ended) self.add_hook("console_print", self.handle_console_print) self.add_command(("q", "queue"), self.cmd_lq) self.add_command("afk", self.cmd_afk) self.add_command("here", self.cmd_playing) self.add_command("qversion", self.cmd_qversion) self.add_command(("teamsize", "ts"), self.cmd_teamsize, priority=minqlx.PRI_HIGH) # Commands for debugging self.add_command("qpush", self.cmd_qpush, 5) self.add_command("qadd", self.cmd_qadd, 5, usage="<id>") self.add_command("qupd", self.cmd_qupd, 5) self.version = "1.0" self._queue = [] self._vip_queue = [] self._afk = [] self._tags = {} self.initialize() self.is_red_locked = False self.is_blue_locked = False self.is_push_pending = False self.is_endscreen = False ######## TODO: replace for something better, because ######## loading during the endgame screen might cause bugs self.set_cvar_once("qlx_queueSetAfkPermission", "2") self.set_cvar_once("qlx_queueAFKTag", "^3AFK") self.game_port = minqlx.get_cvar("net_port") self.jointimes = {} self._extended_vip = self.check_if_extended_vip() self.test_logger = minqlx.get_logger() self.qlogger = self._configure_logger()
def _configure_logger(): logger = logging.getLogger("minqlx") logger.setLevel(logging.DEBUG) # File file_path = os.path.join(minqlx.get_cvar("fs_homepath"), "minqlx.log") maxlogs = minqlx.Plugin.get_cvar("qlx_logs", int) maxlogsize = minqlx.Plugin.get_cvar("qlx_logsSize", int) file_fmt = logging.Formatter("(%(asctime)s) [%(levelname)s @ %(name)s.%(funcName)s] %(message)s", "%H:%M:%S") file_handler = RotatingFileHandler(file_path, encoding="utf-8", maxBytes=maxlogsize, backupCount=maxlogs) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_fmt) logger.addHandler(file_handler) logger.info("============================= minqlx run @ {} =============================" .format(datetime.datetime.now())) # Console console_fmt = logging.Formatter("[%(name)s.%(funcName)s] %(levelname)s: %(message)s", "%H:%M:%S") console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_fmt) logger.addHandler(console_handler)
def setup_extended_logger() -> None: discord_logger: logging.Logger = logging.getLogger("discord") discord_logger.setLevel(logging.DEBUG) # File file_path = os.path.join(minqlx.get_cvar("fs_homepath"), "minqlx_discord.log") maxlogs: int = minqlx.Plugin.get_cvar("qlx_logs", int) maxlogsize: int = minqlx.Plugin.get_cvar("qlx_logsSize", int) file_fmt: logging.Formatter = \ logging.Formatter("(%(asctime)s) [%(levelname)s @ %(name)s.%(funcName)s] %(message)s", "%H:%M:%S") file_handler: logging.FileHandler = \ RotatingFileHandler(file_path, encoding="utf-8", maxBytes=maxlogsize, backupCount=maxlogs) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(file_fmt) discord_logger.addHandler(file_handler) # Console console_fmt: logging.Formatter = \ logging.Formatter("[%(name)s.%(funcName)s] %(levelname)s: %(message)s", "%H:%M:%S") console_handler: logging.Handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_fmt) discord_logger.addHandler(console_handler)
def __init__(self): self.add_hook("new_game", self.handle_new_game) # self.add_hook("game_end", self.handle_game_end) self.add_hook("player_loaded", self.handle_player_loaded) self.add_hook("player_disconnect", self.handle_player_disconnect) # Commands for debugging # self.add_command("qpush", self.cmd_qpush, 5) # self.add_command("qadd", self.cmd_qadd, 5, usage="<id>") # self.add_command("qupd", self.cmd_qupd, 5) self._queue = [] self._vip_queue = [] # self.initialize() self.is_endscreen = False ######## TODO: replace for something better, because ######## loading during the endgame screen might cause bugs # self.set_cvar_once("qlx_queueSetAfkPermission", "2") # self.set_cvar_once("qlx_queueAFKTag", "^3AFK") self.game_port = minqlx.get_cvar("net_port") self.jointimes = {} self._extended_vip = self.checkIfExtendedVipEnabled() self.qlogger = self._configure_logger()
def __init__(self): self.add_hook("player_connect", self.handle_player_connect, priority=minqlx.PRI_LOWEST) self.add_hook("player_disconnect", self.handle_player_disconnect, priority=minqlx.PRI_LOWEST) self.add_hook("chat", self.handle_chat, priority=minqlx.PRI_LOWEST) self.add_hook("command", self.handle_command, priority=minqlx.PRI_LOWEST) self.set_cvar_once("qlx_chatlogs", "3") self.set_cvar_once("qlx_chatlogsSize", str(3*10**6)) # 3 MB self.chatlog = logging.Logger(__name__) file_dir = os.path.join(minqlx.get_cvar("fs_homepath"), "chatlogs") if not os.path.isdir(file_dir): os.makedirs(file_dir) file_path = os.path.join(file_dir, "chat.log") maxlogs = minqlx.Plugin.get_cvar("qlx_chatlogs", int) maxlogsize = minqlx.Plugin.get_cvar("qlx_chatlogsSize", int) file_fmt = logging.Formatter("[%(asctime)s] %(message)s", "%Y-%m-%d %H:%M:%S") file_handler = RotatingFileHandler(file_path, encoding="utf-8", maxBytes=maxlogsize, backupCount=maxlogs) file_handler.setFormatter(file_fmt) self.chatlog.addHandler(file_handler) self.chatlog.info("============================= Logger started @ {} =============================" .format(datetime.datetime.now()))
def load_preset_plugins(): plugins_temp = [] for p in minqlx.Plugin.get_cvar("qlx_plugins", list): if p == "DEFAULT": plugins_temp += list(DEFAULT_PLUGINS) else: plugins_temp.append(p) plugins = [] for p in plugins_temp: if p not in plugins: plugins.append(p) plugins_path = os.path.abspath(minqlx.get_cvar("qlx_pluginsPath")) plugins_dir = os.path.basename(plugins_path) if os.path.isdir(plugins_path): plugins = [p for p in plugins if "{}.{}".format(plugins_dir, p)] for p in plugins: load_plugin(p) else: raise(PluginLoadError("Cannot find the plugins directory '{}'." .format(os.path.abspath(plugins_path))))
def load_plugin(plugin: str) -> None: logger = get_logger(None) logger.info("Loading plugin '%s'...", plugin) # noinspection PyProtectedMember plugins = minqlx.Plugin._loaded_plugins # pylint: disable=protected-access plugins_path_cvar = minqlx.get_cvar("qlx_pluginsPath") if plugins_path_cvar is None: raise PluginLoadError("cvar qlx_pluginsPath misconfigured") plugins_path = os.path.abspath(plugins_path_cvar) plugins_dir = os.path.basename(plugins_path) if not os.path.isfile(os.path.join(plugins_path, plugin + ".py")): raise PluginLoadError("No such plugin exists.") if plugin in plugins: reload_plugin(plugin) return try: module = importlib.import_module(f"{plugins_dir}.{plugin}") # We add the module regardless of whether it fails or not, otherwise we can't reload later. _modules[plugin] = module if not hasattr(module, plugin): raise PluginLoadError( "The plugin needs to have a class with the exact name as the file, minus the .py." ) plugin_class = getattr(module, plugin) if issubclass(plugin_class, minqlx.Plugin): plugins[plugin] = plugin_class() else: raise PluginLoadError( "Attempted to load a plugin that is not a subclass of 'minqlx.Plugin'." ) except: log_exception(plugin) raise
def cmd_excessive_weaps(self, player, msg, channel): if len(msg) < 2: return minqlx.RET_USAGE if msg[1] == "on": minqlx.set_cvar("weapon_reload_sg", "200") minqlx.set_cvar("weapon_reload_rl", "200") minqlx.set_cvar("weapon_reload_rg", "50") minqlx.set_cvar("weapon_reload_prox", "200") minqlx.set_cvar("weapon_reload_pg", "40") minqlx.set_cvar("weapon_reload_ng", "800") minqlx.set_cvar("weapon_reload_mg", "40") minqlx.set_cvar("weapon_reload_hmg", "40") minqlx.set_cvar("weapon_reload_gl", "200") minqlx.set_cvar("weapon_reload_gauntlet", "100") minqlx.set_cvar("weapon_reload_cg", "30") minqlx.set_cvar("weapon_reload_bfg", "75") minqlx.set_cvar("qlx_excessive", "1") self.msg("Excessive weapons are enabled.") if msg[1] == "off": minqlx.console_command("reset weapon_reload_sg") minqlx.console_command("reset weapon_reload_rl") if (minqlx.get_cvar("pmove_airControl")) == "1": minqlx.set_cvar("weapon_reload_rg", "1200") else: minqlx.console_command("reset weapon_reload_rg") minqlx.console_command("reset weapon_reload_prox") minqlx.console_command("reset weapon_reload_pg") minqlx.console_command("reset weapon_reload_ng") minqlx.console_command("reset weapon_reload_mg") minqlx.console_command("reset weapon_reload_hmg") minqlx.console_command("reset weapon_reload_gl") minqlx.console_command("reset weapon_reload_gauntlet") minqlx.console_command("reset weapon_reload_cg") minqlx.console_command("reset weapon_reload_bfg") minqlx.set_cvar("qlx_excessive", "0") self.msg("Excessive weapons are disabled.")
def handle_vote_called(self, caller, vote, args): if not (self.get_cvar("g_allowSpecVote", bool)) and caller.team == "spectator": if caller.privileges == None: caller.tell("You are not allowed to call a vote as spectator.") return minqlx.RET_STOP_ALL if vote.lower() == "infiniteammo": # enables the '/cv infiniteammo [on/off]' command if args.lower() == "off": self.callvote("set g_infiniteAmmo 0", "infinite ammo: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("set g_infiniteAmmo 1", "infinite ammo: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv infiniteammo [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "freecam": # enables the '/cv freecam [on/off]' command if args.lower() == "off": self.callvote("set g_teamSpecFreeCam 0", "team spectator free-cam: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("set g_teamSpecFreeCam 1", "team spectator free-cam: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv freecam [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "floordamage": # enables the '/cv floordamage [on/off]' command if args.lower() == "off": self.callvote("set g_forceDmgThroughSurface 0", "damage through floor: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("set g_forceDmgThroughSurface 1", "damage through floor: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv floordamage [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "alltalk": # enables the '/cv alltalk [on/off]' command if args.lower() == "off": self.callvote("set g_allTalk 0", "voice comm between teams: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("set g_allTalk 1", "voice comm between teams: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv alltalk [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "allready": # enables the '/cv allready' command if self.game.state == "warmup": self.callvote("allready", "begin game immediately") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("You can't vote to begin the game when the game is already on.") return minqlx.RET_STOP_ALL if vote.lower() == "ruleset": # enables the '/cv ruleset [pql/vql]' command if (minqlx.get_cvar("qlx_rulesetLocked")) == "1": caller.tell("Voting to change the ruleset is disabled on ruleset-locked servers.") return minqlx.RET_STOP_ALL if args.lower() == "pql": self.callvote("qlx !ruleset pql", "ruleset: pql") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "vql": self.callvote("qlx !ruleset vql", "ruleset: vql") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv ruleset [pql/vql]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "abort": # enables the '/cv abort' command if self.game.state != "warmup": self.callvote("abort", "abort the game", 30) self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("You can't vote to abort the game when the game isn't in progress.") return minqlx.RET_STOP_ALL if vote.lower() == "chatsounds": # enables the '/cv chatsounds [on/off]' command if args.lower() == "off": self.callvote("qlx !unload fun", "chat-activated sounds: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("qlx !load fun", "chat-activated sounds: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv chatsounds [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() in ("silence", "mute"): # enables the '/cv silence <id>' command try: player_name = self.player(int(args)).clean_name player_id = self.player(int(args)).id except: caller.tell("^1Invalid ID.^7 Use a client ID from the ^2/players^7 command.") return minqlx.RET_STOP_ALL if self.get_cvar("qlx_serverExemptFromModeration") == "1": caller.tell("This server has the serverExemptFromModeration flag set, and therefore, silencing is disabled.") return minqlx.RET_STOP_ALL self.callvote("qlx !silence {} 10 minutes You were call-voted silent for 10 minutes.; mute {}".format(player_id, player_id), "silence {} for 10 minutes".format(player_name)) self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if vote.lower() == "tempban": # enables the '/cv tempban <id>' command if self.get_cvar("qlx_disablePlayerRemoval", bool): # if player removal cvar is set, do not permit '/cv tempban' if caller.privileges == None: caller.tell("Voting to tempban is disabled in this server.") caller.tell("^2/cv spec <id>^7 and ^2/cv silence <id>^7 exist as substitutes to kicking/tempbanning.") return minqlx.RET_STOP_ALL try: player_name = self.player(int(args)).clean_name player_id = self.player(int(args)).id except: caller.tell("^1Invalid ID.^7 Use a client ID from the ^2/players^7 command.") return minqlx.RET_STOP_ALL if self.player(int(args)).privileges != None: caller.tell("The player specified is an admin, a mod or banned, and cannot be tempbanned.") return minqlx.RET_STOP_ALL self.callvote("tempban {}".format(player_id), "^1ban {} until the map changes^3".format(player_name)) self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if vote.lower() == "spec": # enables the '/cv spec <id>' command try: player_name = self.player(int(args)).clean_name player_id = self.player(int(args)).id except: caller.tell("^1Invalid ID.^7 Use a client ID from the ^2/players^7 command.") return minqlx.RET_STOP_ALL if self.player(int(args)).team == "spectator": caller.tell("That player is already in the spectators.") return minqlx.RET_STOP_ALL self.callvote("put {} spec".format(player_id), "move {} to the spectators".format(player_name)) self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if vote.lower() == "excessive": # enables the '/cv excessive [on/off]' command if args.lower() == "off": self.callvote("qlx !excessiveweaps off", "excessive weapons: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("qlx !excessiveweaps on", "excessive weapons: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv excessive [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() in ("kick", "clientkick"): # if player removal cvar is set, do not permit '/cv kick' or '/cv clientkick' if self.get_cvar("qlx_disablePlayerRemoval", bool): if caller.privileges == None: caller.tell("Voting to kick/clientkick is disabled in this server.") caller.tell("^2/cv spec <id>^7 and ^2/cv silence <id>^7 exist as substitutes to kicking.") return minqlx.RET_STOP_ALL if vote.lower() == "lock": # enables the '/cv lock <team>' command if len(args) <= 1: self.callvote("lock", "lock all teams") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: if args.lower() == "blue": self.callvote("lock blue", "lock the ^4blue^3 team") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "red": self.callvote("lock red", "lock the ^1red^3 team") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv lock^7 or ^2/cv lock <blue/red>^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "unlock": # enables the '/cv unlock <team>' command if len(args) <= 1: self.callvote("unlock", "unlock all teams") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: if args.lower() == "blue": self.callvote("unlock blue", "unlock the ^4blue^3 team") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "red": self.callvote("unlock red", "unlock the ^1red^3 team") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv unlock^7 or ^2/cv unlock <blue/red>^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "balancing": # enables the '/cv balancing on/off' command if args.lower() == "off": self.callvote("qlx !unload balance", "glicko-based team balancing: off") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL elif args.lower() == "on": self.callvote("qlx !load balance", "glicko-based team balancing: on") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv balancing [on/off]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "roundtimelimit": # enables the '/cv roundtimelimit [90/120/180]' command if args.lower() == "180": self.callvote("set roundtimelimit 180", "round time limit: 180") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if args.lower() == "120": self.callvote("set roundtimelimit 120", "round time limit: 120") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if args.lower() == "90": self.callvote("set roundtimelimit 90", "round time limit: 90") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv roundtimelimit [90/120/180]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "balance": # enables the '/cv balance' command self.callvote("qlx !balance", "balance the teams") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if vote.lower() == "lgdamage": # enables the '/cv lgdamage [6/7]' command if args.lower() == "6": self.callvote("set g_damage_lg 6; set g_knockback_lg 1.75", "^7Lightning gun^3 damage: 6") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if args.lower() == "7": self.callvote("set g_damage_lg 7; set g_knockback_lg 1.50", "^7Lightning gun^3 damage: 7 (with appropriate knockback)") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv lgdamage [6/7]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "lgdamage": # enables the '/cv lgdamage [6/7]' command if args.lower() == "6": self.callvote("set g_damage_lg 6; set g_knockback_lg 1.75", "^7Lightning gun^3 damage: 6") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if args.lower() == "7": self.callvote("set g_damage_lg 7; set g_knockback_lg 1.50", "^7Lightning gun^3 damage: 7 (with appropriate knockback)") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv lgdamage [6/7]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "rgdamage": # enables the '/cv rgdamage [80/100]' command if args.lower() == "80": self.callvote("set g_damage_rg 80", "^2Railgun^3 damage: 80") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL if args.lower() == "100": self.callvote("set g_damage_rg 100", "^2Railgun^3 damage: 100") self.msg("{}^7 called a vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^2/cv rgdamage [80/100]^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL if vote.lower() == "cvar": if not self.get_cvar("qlx_disableCvarVoting", bool): if not len(args) <= 1: # enables the '/cv cvar <variable> <value>' command if self.db.has_permission(caller.steam_id, self.get_cvar("qlx_cvarVotePermissionRequired", int)): self.callvote("set {}".format(args), "Server CVAR change: {}^3".format(args)) self.msg("{}^7 called a server vote.".format(caller.name)) return minqlx.RET_STOP_ALL else: caller.tell("^1Insufficient privileges to change a server cvar.^7 Permission Level required: ^43^7.") return minqlx.RET_STOP_ALL else: caller.tell("^2/cv cvar <variable> <value>^7 is the usage for this callvote command.") return minqlx.RET_STOP_ALL else: caller.tell("Voting to change server CVARs is disabled on this server.") return minqlx.RET_STOP_ALL
def process_frame(self): self.frame_counter += 1 if self.frame_counter == (int(self.get_cvar("qlx_pingSpecSecondsBetweenChecks")) * int(minqlx.get_cvar("sv_fps"))): self.frame_counter = 0 self.check_ping()
def tags(self): return minqlx.get_cvar("sv_tags").split(",")
def set_cvar_once(name, value, flags=0): if minqlx.get_cvar(name) is None: minqlx.set_cvar(name, value, flags) return True return False
def set_cvar_limit_once(name, value, minimum, maximum, flags=0): if minqlx.get_cvar(name) is None: minqlx.set_cvar_limit(name, value, minimum, maximum, flags) return True return False