def check_rating_requirements(self, names, channel, game_type): """Checks if someone meets the rating requirements to play on the server.""" config = minqlbot.get_config() min_rating = 0 max_rating = 0 if "Balance" in config: if "MinimumRating" in config["Balance"]: min_rating = int(config["Balance"]["MinimumRating"]) if "MaximumRating" in config["Balance"]: max_rating = int(config["Balance"]["MaximumRating"]) else: return True if not min_rating and not max_rating: return True not_cached = self.not_cached(game_type, names) if not_cached: with self.rlock: for lookup in self.lookups: for n in self.lookups[lookup][1]: if n in not_cached: not_cached.remove(n) if not_cached: self.fetch_player_ratings(not_cached, channel, game_type) if (self.check_rating_requirements, (names, channel, game_type)) not in self.pending: self.pending.append((self.check_rating_requirements, (names, channel, game_type))) return False for name in names: if "real_elo" in self.cache[name][game_type]: rating = self.cache[name][game_type]["real_elo"] else: rating = self.cache[name][game_type]["elo"] if (rating > max_rating and max_rating != 0) or (rating < min_rating and min_rating != 0): allow_spec = config["Balance"].getboolean("AllowSpectators", fallback=True) if allow_spec: p = self.player(name) if p.team != "spectator": self.put(name, "spectator") if rating > max_rating and max_rating != 0: self.tell( "^7Sorry, but you can have at most ^6{}^7 rating to play here and you have ^6{}^7." .format(max_rating, rating), name) elif rating < min_rating and min_rating != 0: self.tell( "^7Sorry, but you need at least ^6{}^7 rating to play here and you have ^6{}^7." .format(min_rating, rating), name) else: self.kickban(name) self.debug( name + " was kicked for not being within the rating requirements." )
def handle_player_connect(self, player): status = self.leave_status(player.name) # Check if a player has been banned for leaving, if we're doing that. if status and status[0] == "ban": self.flag_player(player) player.mute() self.delay(25, lambda: player.tell("^7You have been banned from this server for leaving too many games.")) self.delay(40, player.kickban) # Stop plugins on lowest priority from triggering this event since we're kicking. return minqlbot.RET_STOP # Check if player needs to be warned. elif status and status[0] == "warn": self.delay(12, self.warn_player, args=(player, status[1])) # Check if a player has been banned manually. elif self.is_banned(player.name): self.flag_player(player) self.delay(5, player.kickban) # Stop plugins on lower priority from triggering this event since we're kicking. return minqlbot.RET_STOP config = minqlbot.get_config() if "Ban" in config and "MinimumDaysRegistered" in config["Ban"]: days = int(config["Ban"]["MinimumDaysRegistered"]) if days > 0: threading.Thread(target=self.get_profile_thread, args=(player, days)).start()
def handle_vote_called(self, caller, vote, args): config = minqlbot.get_config() if "Essentials" in config and "AutoPassMajorityVote" in config[ "Essentials"]: auto_pass = config["Essentials"].getboolean("AutoPassMajorityVote") if auto_pass: self.vote_resolve_timer = self.delay(27.5, self.resolve_vote) # Enforce teamsizes. if vote == "teamsize": args = int(args) if "Essentials" in config and "MaximumTeamsize" in config[ "Essentials"]: max_teamsize = int(config["Essentials"]["MaximumTeamsize"]) if args > max_teamsize: self.vote_no() if "Essentials" in config and "MinimumTeamsize" in config[ "Essentials"]: min_teamsize = int(config["Essentials"]["MinimumTeamsize"]) if args < min_teamsize: self.vote_no() elif vote == "kick": if args == minqlbot.NAME.lower(): self.vote_no()
def cache_players(self, ratings, lookup): """Save the ratings of a player to the cache. """ config = minqlbot.get_config() if ratings == None: self.lookup_failed(lookup) return else: floor = 0 ceiling = 0 if "Balance" in config: if "FloorRating" in config["Balance"]: floor = int(config["Balance"]["FloorRating"]) if "CeilingRating" in config["Balance"]: ceiling = int(config["Balance"]["CeilingRating"]) with self.rlock: self.fails = 0 # Reset fail counter. for player in ratings["players"]: name = player["nick"] del player["nick"] for game_type in player: if game_type == "alias_of": # Not a game type. continue # Enforce floor and ceiling values if we have them. if floor and player[game_type]["elo"] < floor: player[game_type]["real_elo"] = player[game_type][ "elo"] player[game_type]["elo"] = floor elif ceiling and player[game_type]["elo"] > ceiling: player[game_type]["real_elo"] = player[game_type][ "elo"] player[game_type]["elo"] = ceiling with self.rlock: # If it's an alias, go ahead and cache the real one as well. if "alias_of" in player: real_name = player["alias_of"] self.cache[real_name] = player.copy() # Make sure real name isn't treated as alias. del self.cache[real_name]["alias_of"] if name not in self.cache: # Already in our cache? self.cache[name] = player else: if "alias_of" in player: self.cache[name]["alias_of"] = player["alias_of"] # Gotta be careful not to overwrite game types we've manually set ratings for. for game_type in player: if game_type not in self.cache[ name] and game_type != "alias_of": self.cache[name][game_type] = player[game_type] # The lookup's been dealt with, so we get rid of it. if lookup: with self.rlock: del self.lookups[lookup.uid]
def fetch_player_ratings(self, names, channel, game_type, use_local=True, use_aliases=True): """Fetch ratings from the database and fall back to QLRanks. Takes into account ongoing lookups to avoid sending multiple requests for a player. """ config = minqlbot.get_config() # Fetch players from the database first if the config is set to do so. if use_local and "Balance" in config and config["Balance"].getboolean( "UseLocalRatings", fallback=False): ratings = {"players": []} # We follow QLRanks' JSON format. for name in names.copy(): c = self.db_query( "SELECT game_type, rating FROM ratings WHERE name=?", name) res = c.fetchall() if res: d = {"nick": name} for row in res: d[row["game_type"]] = { "elo": row["rating"], "rank": -1 } # QLRanks' format. if game_type == row["game_type"]: names.remove(name) # Got the one we need locally. ratings["players"].append(d) if ratings["players"]: self.cache_players(ratings, None) # If we've covered everyone, we execute whatever pending tasks we have. if not names: self.execute_pending() return # Remove players we're already waiting a response for. with self.rlock: for lookup in self.lookups: for n in self.lookups[lookup][1]: if n in names: names.remove(n) # We fall back to QLRanks for players we don't have, but stop if we want a gametype it doesn't provide. if names and game_type in QLRANKS_GAMETYPES: if use_aliases and "Balance" in config: conf_alias = config["Balance"].getboolean("UseAliases", fallback=True) else: conf_alias = False lookup = qlranks.QlRanks(self, names, check_alias=conf_alias) with self.rlock: self.lookups[lookup.uid] = (lookup, names, channel) lookup.start() return True else: return False
def cache_players(self, ratings, lookup): """Save the ratings of a player to the cache. """ config = minqlbot.get_config() if ratings == None: self.lookup_failed(lookup) return else: floor = 0 ceiling = 0 if "Balance" in config: if "FloorRating" in config["Balance"]: floor = int(config["Balance"]["FloorRating"]) if "CeilingRating" in config["Balance"]: ceiling = int(config["Balance"]["CeilingRating"]) with self.lock: self.fails = 0 # Reset fail counter. for player in ratings["players"]: name = player["nick"] del player["nick"] for game_type in player: if game_type == "alias_of": # Not a game type. continue # Enforce floor and ceiling values if we have them. if floor and player[game_type]["elo"] < floor: player[game_type]["real_elo"] = player[game_type]["elo"] player[game_type]["elo"] = floor elif ceiling and player[game_type]["elo"] > ceiling: player[game_type]["real_elo"] = player[game_type]["elo"] player[game_type]["elo"] = ceiling with self.lock: # If it's an alias, go ahead and cache the real one as well. if "alias_of" in player: real_name = player["alias_of"] self.cache[real_name] = player.copy() # Make sure real name isn't treated as alias. del self.cache[real_name]["alias_of"] if name not in self.cache: # Already in our cache? self.cache[name] = player else: if "alias_of" in player: self.cache[name]["alias_of"] = player["alias_of"] # Gotta be careful not to overwrite game types we've manually set ratings for. for game_type in player: if game_type not in self.cache[name] and game_type != "alias_of": self.cache[name][game_type] = player[game_type] # The lookup's been dealt with, so we get rid of it. if lookup: with self.lock: del self.lookups[lookup.uid]
def run(self): while True: timeout = float(minqlbot.get_config()["MaxPing"]["SampleInterval"]) if minqlbot.connection_status() == 8: self.plugin.expecting = True minqlbot.Plugin.scores() if self.__stop.wait(timeout=timeout): break
def handle_vote_called(self, caller, vote, args): config = minqlbot.get_config() if vote == "shuffle" and "Balance" in config: auto_reject = config["Balance"].getboolean("VetoUnevenShuffleVote", fallback=False) if auto_reject: teams = self.teams() if len(teams["red"] + teams["blue"]) % 2 == 1: self.vote_no() self.msg("^7Only call shuffle votes when the total number of players is an even number.")
def is_leaver_banning(self): config = minqlbot.get_config() if ("Ban" in config and "AutomaticLeaveBan" in config["Ban"] and config["Ban"].getboolean("AutomaticLeaveBan") and "MinimumGamesPlayedBeforeBan" in config["Ban"] and "WarnThreshold" in config["Ban"] and "BanThreshold" in config["Ban"]): return True else: return False
def handle_vote_called(self, caller, vote, args): config = minqlbot.get_config() if vote == "shuffle" and "Balance" in config: auto_reject = config["Balance"].getboolean("VetoUnevenShuffleVote", fallback=False) if auto_reject: teams = self.teams() if len(teams["red"] + teams["blue"]) % 2 == 1: self.vote_no() self.msg( "^7Only call shuffle votes when the total number of players is an even number." )
def change_map(self): config = minqlbot.get_config() new_map = "" if "EmptyActions" in config and "MapOnEmpty" in config["EmptyActions"]: try: new_map = random.choice([ s.strip() for s in config["EmptyActions"]["MapOnEmpty"].split(",") ]) except IndexError: return if new_map != "": self.change_map(new_map)
def fetch_player_ratings(self, names, channel, game_type, use_local=True, use_aliases=True): """Fetch ratings from the database and fall back to QLRanks. Takes into account ongoing lookups to avoid sending multiple requests for a player. """ config = minqlbot.get_config() # Fetch players from the database first if the config is set to do so. if use_local and "Balance" in config and config["Balance"].getboolean("UseLocalRatings", fallback=False): ratings = {"players": []} # We follow QLRanks' JSON format. for name in names.copy(): c = self.db_query("SELECT game_type, rating FROM ratings WHERE name=?", name) res = c.fetchall() if res: d = {"nick": name} for row in res: d[row["game_type"]] = {"elo": row["rating"], "rank": -1} # QLRanks' format. if game_type == row["game_type"]: names.remove(name) # Got the one we need locally. ratings["players"].append(d) if ratings["players"]: self.cache_players(ratings, None) # If we've covered everyone, we execute whatever pending tasks we have. if not names: self.execute_pending() return # Remove players we're already waiting a response for. with self.lock: for lookup in self.lookups: for n in self.lookups[lookup][1]: if n in names: names.remove(n) # We fall back to QLRanks for players we don't have, but stop if we want a gametype it doesn't provide. if names and game_type in QLRANKS_GAMETYPES: if use_aliases and "Balance" in config: conf_alias = config["Balance"].getboolean("UseAliases", fallback=True) else: conf_alias = False lookup = qlranks.QlRanks(self, names, check_alias=conf_alias) with self.lock: self.lookups[lookup.uid] = (lookup, names, channel) lookup.start() return True else: return False
def handle_vote_ended(self, vote, args, vote_count, passed): config = minqlbot.get_config() if "Balance" not in config: return if passed == True and vote == "shuffle": auto = config["Balance"].getboolean("AutoBalance", fallback=False) if not auto: return else: teams = self.teams() total = len(teams["red"]) + len(teams["blue"]) if total % 2 == 0: self.delay(5, self.average_balance, args=(minqlbot.CHAT_CHANNEL, self.game().short_type)) else: self.msg("^7I can't balance when the total number of players is not an even number.")
def __init__(self): super().__init__() config = minqlbot.get_config() if ( "MaxPing" not in config or "Samples" not in config["MaxPing"] or "SampleInterval" not in config["MaxPing"] or "MaximumPing" not in config["MaxPing"] ): raise AttributeError('maxping needs a "MaxPing" section with the fields "Samples", "SampleInterval", and "MaximumPing" in the config.') self.add_hook("scores", self.handle_scores) self.add_hook("unload", self.handle_unload) self.pings = {} self.expecting = False # Set to True when we're expecting to receive scores. self.requester = ScoresRequester(self) self.requester.start()
def handle_player_disconnect(self, player, reason): teams = self.teams() count = len(teams["red"]) + len(teams["blue"]) + len( teams["spectator"]) if (count < 2): config = minqlbot.get_config() new_ts = 0 if "EmptyActions" in config and "TSOnEmpty" in config[ "EmptyActions"]: new_ts = int(config["EmptyActions"]["TSOnEmpty"]) if (new_ts > 0 and new_ts <= 8): self.teamsize(new_ts) self.delay(5, self.change_map) else: self.change_map()
def db_connect(self): """Returns a connection for the current thread. """ thread = threading.current_thread() if not self.db_is_connected(thread): db = minqlbot.get_config()["Core"]["DatabasePath"] conn = sqlite3.connect(db) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("PRAGMA foreign_keys = ON") # Enforce foreign keys. with self.db_lock: self.db_connections[thread] = conn return conn else: with self.db_lock: return self.db_connections[thread]
def handle_vote_called(self, caller, vote, args): config = minqlbot.get_config() if "Essentials" in config and "AutoPassMajorityVote" in config["Essentials"]: auto_pass = config["Essentials"].getboolean("AutoPassMajorityVote") if auto_pass: self.vote_resolve_timer = self.delay(27.5, self.resolve_vote) # Enforce teamsizes. if vote == "teamsize": args = int(args) if "Essentials" in config and "MaximumTeamsize" in config["Essentials"]: max_teamsize = int(config["Essentials"]["MaximumTeamsize"]) if args > max_teamsize: self.vote_no() if "Essentials" in config and "MinimumTeamsize" in config["Essentials"]: min_teamsize = int(config["Essentials"]["MinimumTeamsize"]) if args < min_teamsize: self.vote_no()
def handle_scores(self, scores): if not self.expecting: return self.expecting = False config = minqlbot.get_config() for score in scores: name = score.player.clean_name.lower() if name not in self.pings: self.pings[name] = [score.ping] else: self.pings[name].append(score.ping) # Delete samples until we have "Samples" number of samples. # We use a while loop since the config could update and become more than just 1 lower than previously. samples = int(config["MaxPing"]["Samples"]) while len(self.pings[name]) > samples: del self.pings[name][0] self.check_pings(config)
def __init__(self): super().__init__() self.add_hook("unload", self.handle_unload) self.add_hook("chat", self.handle_game_chat) self.add_hook("player_connect", self.handle_player_connect) self.add_hook("player_disconnect", self.handle_player_disconnect) # Static instance so we don't waste resources making a new one every time. self.irc_bot_channel = IrcAdminChannel(self) self.config = minqlbot.get_config() self.server = self.config["IRC"].get("Server", fallback="irc.quakenet.org") self.channel = self.config["IRC"]["Channel"] self.admin_channel = self.config["IRC"]["AdminChannel"] self.admin_channel_pass = self.config["IRC"]["AdminChannelPassword"] self.color_translation = self.config["IRC"].getboolean("TranslateColors", fallback=False) self.irc_name = "QL" + minqlbot.NAME self.irc = SimpleIrc(self.irc_name, "irc.quakenet.org", 6667, self.channel, self.admin_channel, self.admin_channel_pass, self) self.irc_thread = Thread(target=self.irc.run) self.irc_thread.start()
def leave_status(self, name): """Get a player's status when it comes to leaving, given automatic leaver ban is on. """ if not self.is_leaver_banning(): return None c = self.db_query("SELECT * FROM Players WHERE name=?", self.clean_name(name).lower()) row = c.fetchone() if not row: return None config = minqlbot.get_config() min_games_completed = int(config["Ban"]["MinimumGamesPlayedBeforeBan"]) warn_threshold = float(config["Ban"]["WarnThreshold"]) ban_threshold = float(config["Ban"]["BanThreshold"]) # Check their games completed to total games ratio. total = row["games_completed"] + row["games_left"] if not total: return None elif total < min_games_completed: # If they have played less than the minimum, check if they can possibly recover by the time # they have played the minimum amount of games. ratio = (row["games_completed"] + (min_games_completed - total)) / min_games_completed else: ratio = row["games_completed"] / total if ratio <= warn_threshold and (ratio > ban_threshold or total < min_games_completed): action = "warn" elif ratio <= ban_threshold and total >= min_games_completed: action = "ban" else: action = None return (action, ratio)
def __init__(self): super().__init__() self.add_hook("unload", self.handle_unload) self.add_hook("chat", self.handle_game_chat) self.add_hook("player_connect", self.handle_player_connect) self.add_hook("player_disconnect", self.handle_player_disconnect) # Static instance so we don't waste resources making a new one every time. self.irc_bot_channel = IrcAdminChannel(self) self.config = minqlbot.get_config() self.server = self.config["IRC"].get("Server", fallback="irc.quakenet.org") self.channel = self.config["IRC"]["Channel"] self.admin_channel = self.config["IRC"]["AdminChannel"] self.admin_channel_pass = self.config["IRC"]["AdminChannelPassword"] self.color_translation = self.config["IRC"].getboolean( "TranslateColors", fallback=False) self.irc_name = "QL" + minqlbot.NAME self.irc = SimpleIrc(self.irc_name, self.server, 6667, self.channel, self.admin_channel, self.admin_channel_pass, self) self.irc_thread = Thread(target=self.irc.run) self.irc_thread.start()
def check_rating_requirements(self, names, channel, game_type): """Checks if someone meets the rating requirements to play on the server.""" config = minqlbot.get_config() min_rating = 0 max_rating = 0 if "Balance" in config: if "MinimumRating" in config["Balance"]: min_rating = int(config["Balance"]["MinimumRating"]) if "MaximumRating" in config["Balance"]: max_rating = int(config["Balance"]["MaximumRating"]) else: return True if not min_rating and not max_rating: return True not_cached = self.not_cached(game_type, names) if not_cached: with self.rlock: for lookup in self.lookups: for n in self.lookups[lookup][1]: if n in not_cached: not_cached.remove(n) if not_cached: self.fetch_player_ratings(not_cached, channel, game_type) if (self.check_rating_requirements, (names, channel, game_type)) not in self.pending: self.pending.append((self.check_rating_requirements, (names, channel, game_type))) return False for name in names: if "real_elo" in self.cache[name][game_type]: rating = self.cache[name][game_type]["real_elo"] else: rating = self.cache[name][game_type]["elo"] if (rating > max_rating and max_rating != 0) or (rating < min_rating and min_rating != 0): allow_spec = config["Balance"].getboolean("AllowSpectators", fallback=True) if allow_spec: player = self.player(name) if not player: return True if player.team != "spectator": self.put(name, "spectator") if rating > max_rating and max_rating != 0: self.tell( "^7Sorry, but you can have at most ^6{}^7 rating to play here and you have ^6{}^7.".format( max_rating, rating ), name, ) elif rating < min_rating and min_rating != 0: self.tell( "^7Sorry, but you need at least ^6{}^7 rating to play here and you have ^6{}^7.".format( min_rating, rating ), name, ) else: player = self.player(name) if not player: return True elif player.team != "spectator": self.put(player, "spectator") self.flag_player(player) player.mute() self.delay( 25, lambda: player.tell( "^7You do not meet the rating requirements on this server. You will be kicked shortly." ), ) self.delay(40, player.kickban) return True
def teams_info(self, channel, game_type): """Send average team ratings and an improvement suggestion to whoever asked for it. """ teams = self.teams() diff = len(teams["red"]) - len(teams["blue"]) if diff: channel.reply("^7Both teams should have the same number of players.") return True players = teams["red"] + teams["blue"] not_cached = self.not_cached(game_type, players) if not_cached: with self.rlock: for lookup in self.lookups: for n in self.lookups[lookup][1]: if n in not_cached: not_cached.remove(n) if not_cached: self.fetch_player_ratings(not_cached, channel, game_type) if (self.teams_info, (channel, game_type)) not in self.pending: self.pending.append((self.teams_info, (channel, game_type))) # Let a later call to execute_pending come back to us. return False avg_red = self.team_average(teams["red"], game_type) avg_blue = self.team_average(teams["blue"], game_type) switch = self.suggest_switch(teams, game_type) diff = len(teams["red"]) - len(teams["blue"]) diff_rounded = abs(round(avg_red) - round(avg_blue)) # Round individual averages. if round(avg_red) > round(avg_blue): channel.reply("^1{} ^7vs ^4{}^7 - DIFFERENCE: ^1{}".format(round(avg_red), round(avg_blue), diff_rounded)) elif round(avg_red) < round(avg_blue): channel.reply("^1{} ^7vs ^4{}^7 - DIFFERENCE: ^4{}".format(round(avg_red), round(avg_blue), diff_rounded)) else: channel.reply("^1{} ^7vs ^4{}^7 - Holy shit!".format(round(avg_red), round(avg_blue))) config = minqlbot.get_config() if "Balance" in config: minimum_suggestion_diff = int(config["Balance"].get("MinimumSuggestionDifference", fallback="25")) else: minimum_suggestion_diff = 25 if switch and switch[1] >= minimum_suggestion_diff: channel.reply( "^7SUGGESTION: switch ^6{}^7 with ^6{}^7. Type !a to agree.".format( switch[0][0].clean_name, switch[0][1].clean_name ) ) if ( not self.suggested_pair or self.suggested_pair[0] != switch[0][0] or self.suggested_pair[1] != switch[0][1] ): self.suggested_pair = (switch[0][0], switch[0][1]) self.suggested_agree = [False, False] else: i = random.randint(0, 99) if not i: channel.reply("^7Teens look ^6good!") else: channel.reply("^7Teams look good!") self.suggested_pair = None return True
def teams_info(self, channel, game_type): """Send average team ratings and an improvement suggestion to whoever asked for it. """ teams = self.teams() diff = len(teams["red"]) - len(teams["blue"]) if diff: channel.reply( "^7Both teams should have the same number of players.") return True players = teams["red"] + teams["blue"] not_cached = self.not_cached(game_type, players) if not_cached: with self.rlock: for lookup in self.lookups: for n in self.lookups[lookup][1]: if n in not_cached: not_cached.remove(n) if not_cached: self.fetch_player_ratings(not_cached, channel, game_type) if (self.teams_info, (channel, game_type)) not in self.pending: self.pending.append( (self.teams_info, (channel, game_type))) # Let a later call to execute_pending come back to us. return False avg_red = self.team_average(teams["red"], game_type) avg_blue = self.team_average(teams["blue"], game_type) switch = self.suggest_switch(teams, game_type) diff = len(teams["red"]) - len(teams["blue"]) diff_rounded = abs(round(avg_red) - round(avg_blue)) # Round individual averages. if round(avg_red) > round(avg_blue): channel.reply("^1{} ^7vs ^4{}^7 - DIFFERENCE: ^1{}".format( round(avg_red), round(avg_blue), diff_rounded)) elif round(avg_red) < round(avg_blue): channel.reply("^1{} ^7vs ^4{}^7 - DIFFERENCE: ^4{}".format( round(avg_red), round(avg_blue), diff_rounded)) else: channel.reply("^1{} ^7vs ^4{}^7 - Holy shit!".format( round(avg_red), round(avg_blue))) config = minqlbot.get_config() if "Balance" in config: minimum_suggestion_diff = int(config["Balance"].get( "MinimumSuggestionDifference", fallback="25")) else: minimum_suggestion_diff = 25 if switch and switch[1] >= minimum_suggestion_diff: channel.reply( "^7SUGGESTION: switch ^6{}^7 with ^6{}^7. Type !a to agree.". format(switch[0][0].clean_name, switch[0][1].clean_name)) if not self.suggested_pair or self.suggested_pair[0] != switch[0][ 0] or self.suggested_pair[1] != switch[0][1]: self.suggested_pair = (switch[0][0], switch[0][1]) self.suggested_agree = [False, False] else: i = random.randint(0, 99) if not i: channel.reply("^7Teens look ^6good!") else: channel.reply("^7Teams look good!") self.suggested_pair = None return True