class MapsRecorder:
    def __init__(self, rcon: RecordedRcon):
        self.rcon = rcon
        self.red = redis.Redis(connection_pool=get_redis_pool())
        self.maps_history = MapsHistory()
        self.prev_map = None
        self._restore_state()

    def _restore_state(self):
        current_map = self.rcon.get_map()
        try:
            last = self.maps_history[0]
        except IndexError:
            logger.warning("Map history is empty, can't restore state")
            return

        started_time = datetime.fromtimestamp(last.get("start"))
        elapsed_time = datetime.now() - started_time

        if last.get("name") == current_map and elapsed_time < timedelta(
                hours=2):
            logging.info("State recovered successfully")
            self.prev_map = current_map
        else:
            logging.warning(
                "The map recorder was offline for too long, the maps history will have gaps"
            )

    def detect_map_change(self):
        try:
            current_map = self.rcon.get_map()
        except CommandFailedError:
            logger.info("Faied to get current map. Skipping")
            return
        logger.debug("Checking for map change current: %s prev: %s",
                     current_map, self.prev_map)
        if self.prev_map != current_map:
            if self.prev_map in ALL_MAPS:
                self.maps_history.save_map_end(self.prev_map)
            if current_map in ALL_MAPS:
                self.maps_history.save_new_map(current_map)

            logger.info(
                "Map change detected updating state. Prev map %s New Map %s",
                self.prev_map,
                current_map,
            )
            if not os.getenv('SILENT_MAP_RECORDER', None):
                send_to_discord_audit(
                    f"map change detected {dict_to_discord(dict(previous=self.prev_map, new=current_map))}",
                    by="MAP_RECORDER",
                    silent=False)
            self.prev_map = current_map
            self.last_map_change_time = datetime.now()
            return True

        return False
Exemple #2
0
def public_info(request):
    status = ctl.get_status()
    try:
        current_map = MapsHistory()[0]
    except IndexError:
        logger.error(
            "Can't get current map time, map_recorder is probably offline")
        current_map = {"name": status["map"], "start": None, "end": None}
    current_map = dict(
        just_name=map_name(current_map["name"]),
        human_name=LONG_HUMAN_MAP_NAMES.get(current_map["name"],
                                            current_map["name"]),
        **current_map,
    )
    vote_status = get_votes_status(none_on_fail=True)
    next_map = ctl.get_next_map()
    return api_response(
        result=dict(
            current_map=current_map,
            **status,
            vote_status=vote_status,
            next_map=next_map,
        ),
        failed=False,
        command="public_info",
    )
Exemple #3
0
    def pick_default_next_map(self):
        selection = self.get_selection()
        maps_history = MapsHistory()
        config = VoteMapConfig()
        all_maps = ALL_MAPS

        if not config.get_votemap_allow_default_to_offsensive():
            logger.debug(
                "Not allowing default to offensive, removing all offensive maps"
            )
            selection = [m for m in selection if not "offensive" in m]
            all_maps = [m for m in ALL_MAPS if not "offensive" in m]

        if not maps_history:
            raise ValueError("Map history is empty")

        return {
            DefaultMethods.least_played_suggestions:
            partial(self.pick_least_played_map, selection),
            DefaultMethods.least_played_all_maps:
            partial(self.pick_least_played_map, all_maps),
            DefaultMethods.random_all_maps:
            lambda: random.choice(
                list(set(all_maps) - set([maps_history[0]["name"]]))),
            DefaultMethods.random_suggestions:
            lambda: random.choice(
                list(set(selection) - set([maps_history[0]["name"]]))),
        }[config.get_votemap_default_method()]()
Exemple #4
0
    def gen_selection(self):
        config = VoteMapConfig()

        logger.debug(
            f"""Generating new map selection for vote map with the following criteria:
            {ALL_MAPS}
            {config.get_votemap_number_of_options()=} 
            {config.get_votemap_ratio_of_offensives_to_offer()=} 
            {config.get_votemap_number_of_last_played_map_to_exclude()=} 
            {config.get_votemap_consider_offensive_as_same_map()=} 
            {config.get_votemap_allow_consecutive_offensives()=} 
            {config.get_votemap_allow_consecutive_offensives_of_opposite_side()=} 
            {config.get_votemap_default_method()=}
        """)
        selection = suggest_next_maps(
            MapsHistory(),
            ALL_MAPS,
            selection_size=config.get_votemap_number_of_options(),
            exclude_last_n=config.
            get_votemap_number_of_last_played_map_to_exclude(),
            offsensive_ratio=config.get_votemap_ratio_of_offensives_to_offer(),
            consider_offensive_as_same_map=config.
            get_votemap_consider_offensive_as_same_map(),
            allow_consecutive_offensive=config.
            get_votemap_allow_consecutive_offensives(),
            allow_consecutive_offensives_of_opposite_side=config.
            get_votemap_allow_consecutive_offensives_of_opposite_side(),
            current_map=self.get_current_map(),
        )
        self.red.delete("MAP_SELECTION")
        self.red.lpush("MAP_SELECTION", *selection)
        logger.info("Saved new selection: %s", selection)
Exemple #5
0
    def register_vote(self, player_name, vote_timestamp, vote_content):
        try:
            current_map = MapsHistory()[0]
            min_time = current_map["start"]
        except IndexError:
            logger.error("Map history is empty - Can't register vote")
            raise
        except KeyError:
            logger.error("Map history is corrupted - Can't register vote")
            raise

        if vote_timestamp < min_time:
            logger.warning(
                f"Vote is too old {player_name=}, {vote_timestamp=}, {vote_content=}, {current_map=}"
            )

        selection = self.get_selection()

        try:
            vote_idx = int(vote_content)
            selected_map = selection[vote_idx]
            self.red.hset("VOTES", player_name, selected_map)
        except (TypeError, ValueError, IndexError):
            raise InvalidVoteError(
                f"Vote must be a number between 0 and {len(selection) - 1}")

        logger.info(
            f"Registered vote from {player_name=} for {selected_map=} - {vote_content=}"
        )
        return selected_map
Exemple #6
0
def current_game_stats():
    try:
        current_map = MapsHistory()[0]
    except IndexError:
        logger.error("No maps information available")
        return {}

    return TimeWindowStats().get_players_stats_from_time(current_map["start"])
Exemple #7
0
    def pick_least_played_map(self, maps):
        maps_history = MapsHistory()

        if not maps:
            raise ValueError("Can't pick a default. No maps to pick from")

        history = [obj["name"] for obj in maps_history]
        index = 0
        for name in maps:
            try:
                idx = history.index(name)
            except ValueError:
                return name
            index = max(idx, index)

        return history[index]
Exemple #8
0
def get_map_history(request):
    data = _get_data(request)
    res = MapsHistory()[:]
    if data.get("pretty"):
        res = [
            dict(
                name=i["name"],
                start=datetime.datetime.fromtimestamp(i["start"]).isoformat()
                if i["start"] else None,
                end=datetime.datetime.fromtimestamp(i["end"]).isoformat()
                if i["end"] else None,
            ) for i in res
        ]
    return api_response(result=res,
                        command="get_map_history",
                        arguments={},
                        failed=False)
Exemple #9
0
def on_map_change(old_map: str, new_map: str):
    logger.info("Running on_map_change hooks with %s %s", old_map, new_map)
    try:
        config = VoteMapConfig()

        if config.get_vote_enabled():
            votemap = VoteMap()
            votemap.gen_selection()
            votemap.clear_votes()
            votemap.apply_with_retry(nb_retry=4)
            #temporary_welcome_in(
            #    "%s{votenextmap_vertical}" % config.get_votemap_instruction_text(),
            #    seconds=60 * 20,
            #    restore_after_seconds=60 * 5,
            #)
    except Exception:
        logger.exception("Unexpected error while running vote map")
    try:
        record_stats_worker(MapsHistory()[1])
    except Exception:
        logger.exception("Unexpected error while running stats worker")
#!/usr/bin/env python

from sqlalchemy.orm import with_expression
from rcon.models import enter_session
from rcon.workers import _record_stats
from rcon.utils import MapsHistory
import sys

with enter_session() as sess:
    if not sys.argv[-1] == 'skiperase':
        sess.execute('truncate table map_history cascade')
        sess.commit()
    for m in MapsHistory():
        try:
            if m['start'] and m['end']:
                _record_stats(m)
        except Exception as e:
            print(f"Unable to process stats for {m}: {repr(2)}")
Exemple #11
0
 def __init__(self, rcon: RecordedRcon):
     self.rcon = rcon
     self.red = redis.Redis(connection_pool=get_redis_pool())
     self.maps_history = MapsHistory()
     self.prev_map = None
     self._restore_state()
Exemple #12
0
class VoteMap:
    def __init__(self) -> None:
        self.red = get_redis_client()

    def is_vote(self, message):
        match = re.match("(\d)", message)
        if not match:
            match = re.match("!votemap\s*(.*)", message)
        if not match:
            return False
        if not match.groups():
            raise InvalidVoteError("You must specify the number of the map")

        return match.groups()[0]

    def register_vote(self, player_name, vote_timestamp, vote_content):
        try:
            current_map = MapsHistory()[0]
            min_time = current_map["start"]
        except IndexError:
            logger.error("Map history is empty - Can't register vote")
            raise
        except KeyError:
            logger.error("Map history is corrupted - Can't register vote")
            raise

        if vote_timestamp < min_time:
            logger.warning(
                f"Vote is too old {player_name=}, {vote_timestamp=}, {vote_content=}, {current_map=}"
            )

        selection = self.get_selection()

        try:
            vote_idx = int(vote_content)
            selected_map = selection[vote_idx]
            self.red.hset("VOTES", player_name, selected_map)
        except (TypeError, ValueError, IndexError):
            raise InvalidVoteError(
                f"Vote must be a number between 0 and {len(selection) - 1}")

        logger.info(
            f"Registered vote from {player_name=} for {selected_map=} - {vote_content=}"
        )
        return selected_map

    def get_vote_overview(self):
        try:
            votes = self.get_votes()
            maps = Counter(votes.values()).most_common()
            return {"total_votes": len(votes), "winning_maps": maps}
        except Exception:
            logger.exception("Can't produce vote overview")

    def clear_votes(self):
        self.red.delete("VOTES")

    def get_votes(self):
        votes = self.red.hgetall("VOTES") or {}
        return {k.decode(): v.decode() for k, v in votes.items()}

    def get_current_map(self):
        map_ = RecordedRcon(SERVER_INFO).get_map()
        if map_.endswith("_RESTART"):
            map_ = map_.replace("_RESTART", "")

        if map_ not in ALL_MAPS:
            raise ValueError("Invalid current map %s map_")

        return map_

    def gen_selection(self):
        config = VoteMapConfig()

        logger.debug(
            f"""Generating new map selection for vote map with the following criteria:
            {ALL_MAPS}
            {config.get_votemap_number_of_options()=} 
            {config.get_votemap_ratio_of_offensives_to_offer()=} 
            {config.get_votemap_number_of_last_played_map_to_exclude()=} 
            {config.get_votemap_consider_offensive_as_same_map()=} 
            {config.get_votemap_allow_consecutive_offensives()=} 
            {config.get_votemap_allow_consecutive_offensives_of_opposite_side()=} 
            {config.get_votemap_default_method()=}
        """)
        selection = suggest_next_maps(
            MapsHistory(),
            ALL_MAPS,
            selection_size=config.get_votemap_number_of_options(),
            exclude_last_n=config.
            get_votemap_number_of_last_played_map_to_exclude(),
            offsensive_ratio=config.get_votemap_ratio_of_offensives_to_offer(),
            consider_offensive_as_same_map=config.
            get_votemap_consider_offensive_as_same_map(),
            allow_consecutive_offensive=config.
            get_votemap_allow_consecutive_offensives(),
            allow_consecutive_offensives_of_opposite_side=config.
            get_votemap_allow_consecutive_offensives_of_opposite_side(),
            current_map=self.get_current_map(),
        )
        self.red.delete("MAP_SELECTION")
        self.red.lpush("MAP_SELECTION", *selection)
        logger.info("Saved new selection: %s", selection)

    def get_selection(self):
        return [v.decode() for v in self.red.lrange("MAP_SELECTION", 0, -1)]

    def pick_least_played_map(self, maps):
        maps_history = MapsHistory()

        if not maps:
            raise ValueError("Can't pick a default. No maps to pick from")

        history = [obj["name"] for obj in maps_history]
        index = 0
        for name in maps:
            try:
                idx = history.index(name)
            except ValueError:
                return name
            index = max(idx, index)

        return history[index]

    def pick_default_next_map(self):
        selection = self.get_selection()
        maps_history = MapsHistory()
        config = VoteMapConfig()
        all_maps = ALL_MAPS

        if not config.get_votemap_allow_default_to_offsensive():
            logger.debug(
                "Not allowing default to offensive, removing all offensive maps"
            )
            selection = [m for m in selection if not "offensive" in m]
            all_maps = [m for m in ALL_MAPS if not "offensive" in m]

        if not maps_history:
            raise ValueError("Map history is empty")

        return {
            DefaultMethods.least_played_suggestions:
            partial(self.pick_least_played_map, selection),
            DefaultMethods.least_played_all_maps:
            partial(self.pick_least_played_map, all_maps),
            DefaultMethods.random_all_maps:
            lambda: random.choice(
                list(set(all_maps) - set([maps_history[0]["name"]]))),
            DefaultMethods.random_suggestions:
            lambda: random.choice(
                list(set(selection) - set([maps_history[0]["name"]]))),
        }[config.get_votemap_default_method()]()

    def apply_results(self):
        config = VoteMapConfig()
        votes = self.get_votes()
        first = Counter(votes.values()).most_common(1)
        if not first:
            next_map = self.pick_default_next_map()
            logger.warning(
                "No votes recorded, defaulting with %s using default winning map %s",
                config.get_votemap_default_method(),
                next_map,
            )
        else:
            logger.info(f"{votes=}")
            next_map = first[0][0]
            if next_map not in ALL_MAPS:
                raise ValueError(
                    f"{next_map=} is not part of the all map list {ALL_MAPS=}")
            if next_map not in (selection := self.get_selection()):
                raise ValueError(
                    f"{next_map=} is not part of vote select {selection=}")
            logger.info(f"Winning map {next_map=}")

        # Sanity checks below
        rcon = RecordedRcon(SERVER_INFO)
        current_map = rcon.get_map()
        if current_map.replace("_RESTART", "") not in ALL_MAPS:
            raise ValueError(
                f"{current_map=} is not part of the all map list {ALL_MAPS=}")

        try:
            history_current_map = MapsHistory()[0]["name"]
        except (IndexError, KeyError):
            raise ValueError("History is empty")

        if current_map != history_current_map:
            raise ValueError(
                f"{current_map=} does not match history map {history_current_map=}"
            )

        # Apply rotation safely
        current_rotation = rcon.get_map_rotation()
        try:
            current_map_idx = current_rotation.index(
                current_map.replace("_RESTART", ""))
        except ValueError:
            logger.warning(
                f"{current_map=} is not in {current_rotation=} will try to add"
            )
            rcon.do_add_map_to_rotation(current_map.replace("_RESTART", ""))

        current_rotation = rcon.get_map_rotation()
        try:
            current_map_idx = current_rotation.index(
                current_map.replace("_RESTART", ""))
        except ValueError as e:
            raise ValueError(
                f"{current_map=} is not in {current_rotation=} addint it failed"
            ) from e

        try:
            next_map_idx = current_rotation.index(next_map)
        except ValueError:
            logger.info(
                f"{next_map=} no in rotation, adding it {current_rotation=}")
            rcon.do_add_map_to_rotation(next_map)
        else:
            if next_map_idx < current_map_idx:
                logger.info(
                    f"{next_map=} is before {current_map=} removing and re-adding"
                )
                rcon.do_remove_map_from_rotation(next_map)
                rcon.do_add_map_to_rotation(next_map)

        current_rotation = rcon.get_map_rotation()

        try:
            next_map_idx = current_rotation.index(next_map)
            if next_map_idx < current_map_idx:
                raise ValueError(f"{next_map=} is still before {current_map=}")
        except ValueError as e:
            raise ValueError(
                f"{next_map=} failed to be added to {current_rotation=}")

        maps_to_remove = current_rotation[current_map_idx + 1:next_map_idx]
        logger.debug(f"{current_map=} {current_rotation=} - {maps_to_remove=}")

        for map in maps_to_remove:
            # Remove the maps that are in between the current map and the desired next map
            rcon.do_remove_map_from_rotation(map)

        for map in maps_to_remove:
            rcon.do_add_map_to_rotation(map)

        # Check that it worked
        current_rotation = rcon.get_map_rotation()
        if (not current_rotation[current_map_idx] == current_map.replace(
                '_RESTART', '')
                and current_rotation[current_map_idx + 1] == next_map):
            raise ValueError(
                f"Applying the winning map {next_map=} after the {current_map=} failed: {current_rotation=}"
            )

        logger.info(
            f"Successfully applied winning mapp {next_map=}, new rotation {current_rotation=}"
        )
        return True