示例#1
0
    class Area:
        """
        Create a new area for the server.
        """
        def __init__(self, area_id, server, parameters):
            """
            Parameters
            ----------
            area_id: int
                The area ID.
            server: server.TsuserverDR
                The server this area belongs to.
            parameters: dict
                Area parameters as specified in the loaded area list.
            """

            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.server = server
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.shoutlog = []
            self.current_music = ''
            self.current_music_player = ''
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.owned = False
            self.ic_lock = False
            self.is_locked = False
            self.is_gmlocked = False
            self.is_modlocked = False
            self.bleeds_to = set()
            self.blood_smeared = False
            self.lights = True
            self.last_ic_messages = list()
            self.parties = set()
            self.dicelog = list()
            self._in_zone = None

            self.name = parameters['area']
            self.background = parameters['background']
            self.bg_lock = parameters['bglock']
            self.evidence_mod = parameters['evidence_mod']
            self.locking_allowed = parameters['locking_allowed']
            self.iniswap_allowed = parameters['iniswap_allowed']
            self.rp_getarea_allowed = parameters['rp_getarea_allowed']
            self.rp_getareas_allowed = parameters['rp_getareas_allowed']
            self.rollp_allowed = parameters['rollp_allowed']
            self.reachable_areas = parameters['reachable_areas']
            self.change_reachability_allowed = parameters[
                'change_reachability_allowed']
            self.default_change_reachability_allowed = parameters[
                'change_reachability_allowed']
            self.gm_iclock_allowed = parameters['gm_iclock_allowed']
            self.afk_delay = parameters['afk_delay']
            self.afk_sendto = parameters['afk_sendto']
            self.lobby_area = parameters['lobby_area']
            self.private_area = parameters['private_area']
            self.scream_range = parameters['scream_range']
            self.restricted_chars = parameters['restricted_chars']
            self.default_description = parameters['default_description']
            self.has_lights = parameters['has_lights']
            self.cbg_allowed = parameters['cbg_allowed']
            self.song_switch_allowed = parameters['song_switch_allowed']
            self.bullet = parameters['bullet']

            # Store the current description separately from the default description
            self.description = self.default_description
            # Have a background backup in order to restore temporary background changes
            self.background_backup = self.background
            # Fix comma-separated entries
            self.reachable_areas = Constants.fix_and_setify(
                self.reachable_areas)
            self.scream_range = Constants.fix_and_setify(self.scream_range)
            self.restricted_chars = Constants.fix_and_setify(
                self.restricted_chars)

            self.default_reachable_areas = self.reachable_areas.copy()
            self.staffset_reachable_areas = self.reachable_areas.copy()

            if '<ALL>' not in self.reachable_areas:
                self.reachable_areas.add(self.name)  # Safety feature, yay sets

            # Make sure only characters that exist are part of the restricted char set
            try:
                for char_name in self.restricted_chars:
                    self.server.char_list.index(char_name)
            except ValueError:
                info = (
                    'Area `{}` has a character `{}` not in the character list of the server '
                    'listed as a restricted character. Please make sure this character exists '
                    'and try again.'.format(self.name, char_name))
                raise AreaError(info)

        def new_client(self, client):
            """
            Add a client to the client list of the current area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to add.
            """

            self.clients.add(client)

        def remove_client(self, client):
            """
            Remove a client of the client list of the current area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to remove.


            Raises
            ------
            KeyError
                If the client is not in the area list.
            """

            try:
                self.clients.remove(client)
            except KeyError:
                if not client.id == -1:  # Ignore pre-clients (before getting playercount)
                    info = 'Area {} does not contain client {}'.format(
                        self, client)
                    raise KeyError(info)

            if not self.clients:
                self.unlock()

        def send_command(self, cmd, *args):
            """
            Send a network packet to all clients in the area.

            Parameters
            ----------
            cmd: str
                ID of the packet.
            *args
                Packet arguments.
            """

            for c in self.clients:
                c.send_command(cmd, *args)

        def broadcast_ooc(self, msg):
            """
            Send an OOC server message to the clients in the area.

            Parameters
            ----------
            msg: str
                Message to be sent.
            """

            self.send_command('CT', self.server.config['hostname'], msg)

        def change_background(self, bg, validate=True, override_blind=False):
            """
            Change the background of the current area.

            Parameters
            ----------
            bg: str
                New background name.
            validate: bool, optional
                Whether to first determine if background name is listed as a server background
                before changing. Defaults to True.
            override_blind: bool, optional
                Whether to send the intended background to blind people as opposed to the server
                blackout one. Defaults to False (send blackout).

            Raises
            ------
            AreaError
                If the server attempted to validate the background name and failed.
            """

            if validate and bg.lower() not in [
                    name.lower() for name in self.server.backgrounds
            ]:
                raise AreaError('Invalid background name.')

            if self.lights:
                self.background = bg
            else:
                self.background = self.server.config['blackout_background']
                self.background_backup = bg
            for c in self.clients:
                if c.is_blind and not override_blind:
                    c.send_background(
                        name=self.server.config['blackout_background'])
                else:
                    c.send_background(name=self.background)

        def get_chars_unusable(self,
                               allow_restricted=False,
                               more_unavail_chars=None):
            """
            Obtain all characters that a player in the current area may NOT change to.

            Parameters
            ----------
            allow_restricted: bool, optional
                Whether to include characters whose usage has been manually restricted in the area.
                Defaults to False.
            more_unavail_chars: set, optional
                Additional characters to mark as taken (and thus unusuable) in the area. Defaults
                to None.

            Returns
            -------
            unavailable: set
                Character IDs of all unavailable characters in the area.
            """

            if more_unavail_chars is None:
                more_unavail_chars = set()

            unavailable = {
                x.char_id
                for x in self.clients
                if x.char_id is not None and x.char_id >= 0
            }
            unavailable |= more_unavail_chars
            restricted = {
                self.server.char_list.index(name)
                for name in self.restricted_chars
            }

            if not allow_restricted:
                unavailable |= restricted

            return unavailable

        def get_rand_avail_char_id(self,
                                   allow_restricted=False,
                                   more_unavail_chars=None):
            """
            Obtain a random available character in the area.

            Parameters
            ----------
            allow_restricted: bool, optional
                Whether to include characters whose usage has been manually restricted in the area.
                Defaults to false.
            more_unavail_chars: set, optional
                Additional characters to mark as taken (and thus unsuable) in the area. Defaults to
                None.

            Returns
            -------
            int
                ID of randomly chosen available character in the area.

            Raises
            -------
            AreaError
                If there are no available characters in the area.
            """

            unusable = self.get_chars_unusable(
                allow_restricted=allow_restricted,
                more_unavail_chars=more_unavail_chars)
            available = {
                i
                for i in range(len(self.server.char_list)) if i not in unusable
            }

            if not available:
                raise AreaError('No available characters.')

            return self.server.random.choice(tuple(available))

        def is_char_available(self,
                              char_id,
                              allow_restricted=False,
                              more_unavail_chars=None):
            """
            Decide whether a character can be selected in the current area.

            Parameters
            ----------
            char_id: int
                ID of the character to test.
            allow_restricted: bool, optional
                Whether to include characters whose usage has been manually restricted in the area.
                Defaults to False.
            more_unavail_chars: set, optional
                Additional characters to mark as taken in the area. Defaults to None.

            Returns
            -------
            bool
                True if tested character ID is the spectator ID (which is always available), or
                is not found to be among the area's unusable characters.
            """

            unused = char_id in self.get_chars_unusable(
                allow_restricted=allow_restricted,
                more_unavail_chars=more_unavail_chars)
            return char_id == -1 or not unused

        def add_to_dicelog(self, client, msg):
            """
            Add a dice roll to the dice log of the area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to record.
            msg: str
                Dice log to record.
            """

            if len(self.dicelog) >= 20:
                self.dicelog = self.dicelog[1:]

            info = '{} | [{}] {} ({}) {}'.format(Constants.get_time(),
                                                 client.id, client.displayname,
                                                 client.get_ip(), msg)
            self.dicelog.append(info)

        def get_dicelog(self):
            """
            Return the dice log of the area.
            """

            info = '== Dice log of area {} ({}) =='.format(self.name, self.id)

            if not self.dicelog:
                info += '\r\nNo dice have been rolled since the area was loaded.'
            else:
                for log in self.dicelog:
                    info += '\r\n*{}'.format(log)
            return info

        def change_doc(self, doc='No document.'):
            """
            Changes the casing document of the area, usually a URL.

            Parameters
            ----------
            doc: str, optional
                New casing document of the area. Defaults to 'No document.'
            """
            self.doc = doc

        def get_evidence_list(self, client):
            """
            Obtain the evidence list for a client.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to target.
            """

            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
            Resend all clients in the area their evidence list.

            Packet format: LE#<name>&<desc>&<img>#<name>
            """

            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))

        def change_hp(self, side, val):
            """
            Change a penalty healthbar.

            Parameters
            ----------
            side: int
                Penalty bar to change (1 for def, 2 for pro).
            val: int
                New health value of the penalty bar.

            Raises
            ------
            AreaError
                If an invalid penalty bar or health value was given.
            """
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')

            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val

            self.send_command('HP', side, val)

        def is_iniswap(self, client, anim1, anim2, char):
            """
            Decide if a client is iniswapping or using files outside their claimed character folder.

            Assumes that server permitted iniswaps do not count as iniswaps.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to test.
            anim1: str
                Location of the preanimation the client used.
            anim2: str
                Location of the main animation the client used.
            char: str
                Name of the folder the client claims their files are.

            Returns
            -------
            bool
                True if either anim1 or anim2 point to an external location through '../../' or
                their claimed character folder does not match the expected server name and the
                performed iniswap is not in the list of allowed iniswaps by the server.
            """

            if char == client.get_char_name():
                return False

            if '..' in anim1 or '..' in anim2:
                return True
            for char_link in self.server.allowed_iniswaps:
                if client.get_char_name() in char_link and char in char_link:
                    return False
            return True

        def add_to_judgelog(self, client, msg):
            """
            Add a judge action to the judge log of the area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to record.
            msg: str
                Judge action to record.
            """

            if len(self.judgelog) >= 20:
                self.judgelog = self.judgelog[1:]

            info = '{} | [{}] {} ({}) {}'.format(Constants.get_time(),
                                                 client.id, client.displayname,
                                                 client.get_ip(), msg)
            self.judgelog.append(info)

        def get_judgelog(self):
            """
            Return the judge log of the area.
            """

            info = '== Judge log of {} ({}) =='.format(self.name, self.id)

            if not self.judgelog:
                info += '\r\nNo judge actions have been performed since the area was loaded.'
            else:
                for log in self.judgelog:
                    info += '\r\n*{}'.format(log)
            return info

        def change_lights(self, new_lights, initiator=None, area=None):
            """
            Change the light status of the area and send related announcements.

            This also updates the light status for parties.

            Parameters
            ----------
            new_lights: bool
                New light status
            initiator: server.ClientManager.Client, optional
                Client who triggered the light status change.
            area: server.AreaManager.Area, optional
                Broadcasts light change messages to chosen area. Used if
                the initiator is elsewhere, such as in /zone_lights.
                If not None, the initiator will receive no notifications of
                light status changes.

            Raises
            ------
            AreaError
                If the new light status matches the current one.
            """

            status = {True: 'on', False: 'off'}
            if self.lights == new_lights:
                raise AreaError('The lights are already turned {}.'.format(
                    status[new_lights]))

            # Change background to match new status
            if new_lights:
                if self.background == self.server.config[
                        'blackout_background']:
                    intended_background = self.background_backup
                else:
                    intended_background = self.background
            else:
                if self.background != self.server.config['blackout_background']:
                    self.background_backup = self.background
                intended_background = self.background

            self.lights = new_lights
            self.change_background(
                intended_background,
                validate=False)  # Allow restoring custom bg.

            # Announce light status change
            if initiator:  # If a player initiated the change light sequence, send targeted messages
                if area is None:
                    if not initiator.is_blind:
                        initiator.send_ooc('You turned the lights {}.'.format(
                            status[new_lights]))
                    elif not initiator.is_deaf:
                        initiator.send_ooc('You hear a flicker.')
                    else:
                        initiator.send_ooc(
                            'You feel a light switch was flipped.')

                initiator.send_ooc_others('The lights were turned {}.'.format(
                    status[new_lights]),
                                          is_zstaff_flex=False,
                                          in_area=area if area else True,
                                          to_blind=False)
                initiator.send_ooc_others('You hear a flicker.',
                                          is_zstaff_flex=False,
                                          in_area=area if area else True,
                                          to_blind=True,
                                          to_deaf=False)
                initiator.send_ooc_others(
                    '(X) {} [{}] turned the lights {}.'.format(
                        initiator.displayname, initiator.id,
                        status[new_lights]),
                    is_zstaff_flex=True,
                    in_area=area if area else True)
            else:  # Otherwise, send generic message
                self.broadcast_ooc('The lights were turned {}.'.format(
                    status[new_lights]))

            # Notify the parties in the area that the lights have changed
            for party in self.parties:
                party.check_lights()

            for c in self.clients:
                c.area_changer.notify_me_blood(self,
                                               changed_visibility=True,
                                               changed_hearing=False)

        def set_next_msg_delay(self, msg_length):
            """
            Set a message delay for the next IC message in the area based on the length of the
            current message, so new messages sent before this delay expires are discarded.

            Parameters
            ----------
            msg_length: int
                Length of the current message.
            """

            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def can_send_message(self):
            """
            Decide if an incoming IC message does not violate the area's established delay for
            the previously received IC message.

            Returns
            -------
            bool
                True if the message was sent after the delay was over.
            """

            return (time.time() * 1000.0 - self.next_message_time) > 0

        def play_track(self,
                       name,
                       client,
                       raise_if_not_found=False,
                       reveal_sneaked=False,
                       pargs=None):
            """
            Wrapper function to play a music track in an area.

            Parameters
            ----------
            name : str
                Name of the track to play
            client : ClientManager.Client
                Client who initiated the track change request.
            effect : int, optional
                Accompanying effect to the track (only used by AO 2.8.4+). Defaults to 0.
            raise_if_not_found : boolean, optional
                If True, it will raise ServerError if the track name is not in the server's music
                list nor the client's music list. If False, it will not care about it. Defaults to
                False.
            reveal_sneaked : boolean, optional
                If True, it will change the visibility status of the sender client to True (reveal
                them). If False, it will keep their visibility as it was. Defaults to False.
            pargs : dict of str to Any
                If given, they are arguments to an MC packet that was given when the track was
                requested, and will override any other arguments given. If not, this is ignored.
                Defaults to None (and converted to an empty dictionary).

            Raises
            ------
            ServerError.MusicNotFoundError:
                If `name` is not a music track in the server or client's music list and
                `raise_if_not_found` is True.
            ServerError (with code 'FileInvalidName')
                If `name` references parent or current directories (e.g. "../hi.mp3")
            """

            if not pargs:
                pargs = dict()
            if False:  # Constants.includes_relative_directories(name):
                info = f'Music names may not reference parent or current directories: {name}'
                raise ServerError(info, code='FileInvalidName')

            try:
                name, length = self.server.get_song_data(name, c=client)
            except ServerError.MusicNotFoundError:
                if raise_if_not_found:
                    raise
                name, length = name, -1

            if 'name' not in pargs:
                pargs['name'] = name
            if 'cid' not in pargs:
                pargs['cid'] = client.char_id
            # if 'showname' not in pargs:
            #     pargs['showname'] = client.displayname
            pargs['showname'] = client.displayname  # Ignore AO shownames
            if 'loop' not in pargs:
                pargs['loop'] = -1
            if 'channel' not in pargs:
                pargs['channel'] = 0
            if 'effects' not in pargs:
                pargs['effects'] = 0

            # self.play_music(name, client.char_id, length, effect=effect)
            def loop(cid):
                for client in self.clients:
                    loop_pargs = pargs.copy()
                    loop_pargs[
                        'cid'] = cid  # Overwrite in case cid changed (e.g., server looping)
                    _, to_send = client.prepare_command('MC', loop_pargs)
                    client.send_command('MC', *to_send)

                if self.music_looper:
                    self.music_looper.cancel()
                if length > 0:
                    f = lambda: loop(-1)  # Server should loop now
                    self.music_looper = asyncio.get_event_loop().call_later(
                        length, f)

            loop(pargs['cid'])

            # Record the character name and the track they played.
            self.current_music_player = client.displayname
            self.current_music = name

            logger.log_server(
                '[{}][{}]Changed music to {}.'.format(self.id,
                                                      client.get_char_name(),
                                                      name), client)

            # Changing music reveals sneaked players, so do that if requested
            if not client.is_staff(
            ) and not client.is_visible and reveal_sneaked:
                client.change_visibility(True)
                client.send_ooc_others(
                    '(X) {} [{}] revealed themselves by playing music ({}).'.
                    format(client.displayname, client.id, client.area.id),
                    is_zstaff=True)

        def play_music(self, name, cid, length=-1):
            """
            Start playing a music track in an area.

            Parameters
            ----------
            name: str
                Name of the track to play.
            cid: int
                Character ID of the player who played the track, or -1 if the server initiated it.
            length: int
                Length of the track in seconds to allow for seamless server-managed looping.
                Defaults to -1 (no looping).
            """

            self.send_command('MC', name, cid)

            if self.music_looper:
                self.music_looper.cancel()
            if length > 0:
                f = lambda: self.play_music(name, -1, length)
                self.music_looper = asyncio.get_event_loop().call_later(
                    length, f)

        def add_to_shoutlog(self, client, msg):
            """
            Add a shout message to the shout log of the area.

            Parameters
            ----------
            client: server.ClientManager.Client
                Client to record.
            msg: str
                Shout message to record.
            """

            if len(self.shoutlog) >= 20:
                self.shoutlog = self.shoutlog[1:]

            info = '{} | [{}] {} ({}) {}'.format(Constants.get_time(),
                                                 client.id, client.displayname,
                                                 client.get_ip(), msg)
            self.shoutlog.append(info)

        def add_party(self, party):
            """
            Adds a party to the area's party list.

            Parameters
            ----------
            party: server.PartyManager.Party
                Party to record.

            Raises
            ------
            AreaError:
                If the party is already a part of the party list.
            """

            if party in self.parties:
                raise AreaError(
                    'Party {} is already part of the party list of this area.'.
                    format(party.get_id()))
            self.parties.add(party)

        def remove_party(self, party):
            """
            Removes a party from the area's party list.

            Parameters
            ----------
            party: server.PartyManager.Party
                Party to record.

            Raises
            ------
            AreaError:
                If the party is not part of the party list.
            """

            if party not in self.parties:
                raise AreaError(
                    'Party {} is not part of the party list of this area.'.
                    format(party.get_id()))
            self.parties.remove(party)

        def get_shoutlog(self):
            """
            Get the shout log of the area.
            """
            info = '== Shout log of {} ({}) =='.format(self.name, self.id)

            if not self.shoutlog:
                info += '\r\nNo shouts have been performed since the area was loaded.'
            else:
                for log in self.shoutlog:
                    info += '\r\n*{}'.format(log)
            return info

        def change_status(self, value):
            """
            Change the casing status of the area to one of predetermined values.

            Parameters
            ----------
            value: str
                New casing status of the area.

            Raises
            ------
            AreaError
                If the new casing status is not among the allowed values.
            """

            allowed_values = [
                'idle', 'building-open', 'building-full', 'casing-open',
                'casing-full', 'recess'
            ]
            if value.lower() not in allowed_values:
                raise AreaError('Invalid status. Possible values: {}'.format(
                    ', '.join(allowed_values)))
            self.status = value.upper()

        def unlock(self):
            """
            Unlock the area so that non-authorized players may now join.
            """

            self.is_locked = False
            if not self.is_gmlocked and not self.is_modlocked:
                self.invite_list = {}

        def gmunlock(self):
            """
            Unlock the area if it had a GM lock so that non-authorized players may now join.
            """

            self.is_gmlocked = False
            self.is_locked = False
            if not self.is_modlocked:
                self.invite_list = {}

        def modunlock(self):
            """
            Unlock the area if it had a mod lock so that non-authorized players may now join.
            """

            self.is_modlocked = False
            self.is_gmlocked = False
            self.is_locked = False
            self.invite_list = {}

        @property
        def in_zone(self):
            """
            Declarator for a public in_zone attribute.
            """

            return self._in_zone

        @in_zone.setter
        def in_zone(self, new_zone_value):
            """
            Set the in_zone parameter to the given one

            Parameters
            ----------
            new_zone_value: ZoneManager.Zone or None
                New zone the area belongs to.

            Raises
            ------
            AreaError:
                If the area was not part of a zone and new_zone_value is None or,
                if the area was part of a zone and new_zone_value is not None.
            """

            if new_zone_value is None and self._in_zone is None:
                raise AreaError('This area is already not part of a zone.')
            if new_zone_value is not None and self._in_zone is not None:
                raise AreaError('This area is already part of a zone.')

            self._in_zone = new_zone_value

        def __repr__(self):
            """
            Return a string representation of the area.

            The string follows the convention 'A::AreaID:AreaName:ClientsInArea'
            """

            return 'A::{}:{}:{}'.format(self.id, self.name, len(self.clients))
示例#2
0
    class Area:
        def __init__(self,
                     area_id,
                     server,
                     name,
                     background,
                     bg_lock,
                     evidence_mod='FFA',
                     locking_allowed=False,
                     iniswap_allowed=True,
                     showname_changes_allowed=False,
                     shouts_allowed=True,
                     jukebox=False,
                     abbreviation='',
                     non_int_pres_only=False):
            self.iniswap_allowed = iniswap_allowed
            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.name = name
            self.background = background
            self.bg_lock = bg_lock
            self.server = server
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.current_music = ''
            self.current_music_player = ''
            self.current_music_player_ipid = -1
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.evidence_mod = evidence_mod
            self.locking_allowed = locking_allowed
            self.showname_changes_allowed = showname_changes_allowed
            self.shouts_allowed = shouts_allowed
            self.abbreviation = abbreviation
            self.cards = dict()
            """
            #debug
            self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
            self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
            self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
            """

            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.non_int_pres_only = non_int_pres_only
            self.jukebox = jukebox
            self.jukebox_votes = []
            self.jukebox_prev_char_id = -1

            self.owners = []

        class Locked(Enum):
            FREE = 1,
            SPECTATABLE = 2,
            LOCKED = 3

        def new_client(self, client):
            self.clients.add(client)
            self.server.area_manager.send_arup_players()

        def remove_client(self, client):
            self.clients.remove(client)
            if len(self.clients) == 0:
                self.change_status('IDLE')

        def unlock(self):
            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.invite_list = {}
            self.server.area_manager.send_arup_lock()
            self.send_host_message('This area is open now.')

        def spectator(self):
            self.is_locked = self.Locked.SPECTATABLE
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            self.server.area_manager.send_arup_lock()
            self.send_host_message('This area is spectatable now.')

        def lock(self):
            self.is_locked = self.Locked.LOCKED
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            self.server.area_manager.send_arup_lock()
            self.send_host_message('This area is locked now.')

        def is_char_available(self, char_id):
            return char_id not in [x.char_id for x in self.clients]

        def get_rand_avail_char_id(self):
            avail_set = set(range(len(self.server.char_list))) - set(
                [x.char_id for x in self.clients])
            if len(avail_set) == 0:
                raise AreaError('No available characters.')
            return random.choice(tuple(avail_set))

        def send_command(self, cmd, *args):
            for c in self.clients:
                c.send_command(cmd, *args)

        def send_owner_command(self, cmd, *args):
            for c in self.owners:
                if not c in self.clients:
                    c.send_command(cmd, *args)

        def send_host_message(self, msg):
            self.send_command('CT', self.server.config['hostname'], msg, '1')
            self.send_owner_command(
                'CT',
                '[' + self.abbreviation + ']' + self.server.config['hostname'],
                msg, '1')

        def set_next_msg_delay(self, msg_length):
            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def is_iniswap(self, client, anim1, anim2, char):
            if self.iniswap_allowed:
                return False
            if '..' in anim1 or '..' in anim2:
                return True
            for char_link in self.server.allowed_iniswaps:
                if client.get_char_name() in char_link and char in char_link:
                    return False
            return True

        def add_jukebox_vote(self, client, music_name, length=-1, showname=''):
            if not self.jukebox:
                return
            if length <= 0:
                self.remove_jukebox_vote(client, False)
            else:
                self.remove_jukebox_vote(client, True)
                self.jukebox_votes.append(
                    self.JukeboxVote(client, music_name, length, showname))
                client.send_host_message('Your song was added to the jukebox.')
                if len(self.jukebox_votes) == 1:
                    self.start_jukebox()

        def remove_jukebox_vote(self, client, silent):
            if not self.jukebox:
                return
            for current_vote in self.jukebox_votes:
                if current_vote.client.id == client.id:
                    self.jukebox_votes.remove(current_vote)
            if not silent:
                client.send_host_message(
                    'You removed your song from the jukebox.')

        def get_jukebox_picked(self):
            if not self.jukebox:
                return
            if len(self.jukebox_votes) == 0:
                return None
            elif len(self.jukebox_votes) == 1:
                return self.jukebox_votes[0]
            else:
                weighted_votes = []
                for current_vote in self.jukebox_votes:
                    i = 0
                    while i < current_vote.chance:
                        weighted_votes.append(current_vote)
                        i += 1
                return random.choice(weighted_votes)

        def start_jukebox(self):
            # There is a probability that the jukebox feature has been turned off since then,
            # we should check that.
            # We also do a check if we were the last to play a song, just in case.
            if not self.jukebox:
                if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID':
                    self.current_music = ''
                return

            vote_picked = self.get_jukebox_picked()

            if vote_picked is None:
                self.current_music = ''
                return

            if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(
                    self.jukebox_votes) > 1:
                self.jukebox_prev_char_id = vote_picked.client.char_id
                if vote_picked.showname == '':
                    self.send_command('MC', vote_picked.name,
                                      vote_picked.client.char_id)
                else:
                    self.send_command('MC', vote_picked.name,
                                      vote_picked.client.char_id,
                                      vote_picked.showname)
            else:
                self.send_command('MC', vote_picked.name, -1)

            self.current_music_player = 'The Jukebox'
            self.current_music_player_ipid = 'has no IPID'
            self.current_music = vote_picked.name

            for current_vote in self.jukebox_votes:
                # Choosing the same song will get your votes down to 0, too.
                # Don't want the same song twice in a row!
                if current_vote.name == vote_picked.name:
                    current_vote.chance = 0
                else:
                    current_vote.chance += 1

            if self.music_looper:
                self.music_looper.cancel()
            self.music_looper = asyncio.get_event_loop().call_later(
                vote_picked.length, lambda: self.start_jukebox())

        def play_music(self, name, cid, length=-1):
            self.send_command('MC', name, cid)
            if self.music_looper:
                self.music_looper.cancel()
            if length > 0:
                self.music_looper = asyncio.get_event_loop().call_later(
                    length, lambda: self.play_music(name, -1, length))

        def play_music_shownamed(self, name, cid, showname, length=-1):
            self.send_command('MC', name, cid, showname)
            if self.music_looper:
                self.music_looper.cancel()
            if length > 0:
                self.music_looper = asyncio.get_event_loop().call_later(
                    length, lambda: self.play_music(name, -1, length))

        def can_send_message(self, client):
            if self.cannot_ic_interact(client):
                client.send_host_message(
                    'This is a locked area - ask the CM to speak.')
                return False
            return (time.time() * 1000.0 - self.next_message_time) > 0

        def cannot_ic_interact(self, client):
            return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list

        def change_hp(self, side, val):
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val
            self.send_command('HP', side, val)

        def change_background(self, bg):
            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError('Invalid background name.')
            self.background = bg
            self.send_command('BN', self.background)

        def change_status(self, value):
            allowed_values = ('idle', 'rp', 'casing', 'looking-for-players',
                              'lfp', 'recess', 'gaming')
            if value.lower() not in allowed_values:
                raise AreaError('Invalid status. Possible values: {}'.format(
                    ', '.join(allowed_values)))
            if value.lower() == 'lfp':
                value = 'looking-for-players'
            self.status = value.upper()
            self.server.area_manager.send_arup_status()

        def change_doc(self, doc='No document.'):
            self.doc = doc

        def add_to_judgelog(self, client, msg):
            if len(self.judgelog) >= 10:
                self.judgelog = self.judgelog[1:]
            self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(),
                                                      client.get_ip(), msg))

        def add_music_playing(self, client, name):
            self.current_music_player = client.get_char_name()
            self.current_music_player_ipid = client.ipid
            self.current_music = name

        def add_music_playing_shownamed(self, client, showname, name):
            self.current_music_player = showname + " (" + client.get_char_name(
            ) + ")"
            self.current_music_player_ipid = client.ipid
            self.current_music = name

        def get_evidence_list(self, client):
            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
                LE#<name>&<desc>&<img>#<name>
                
            """
            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))

        def get_cms(self):
            msg = ''
            for i in self.owners:
                msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', '
            if len(msg) > 2:
                msg = msg[:-2]
            return msg

        class JukeboxVote:
            def __init__(self, client, name, length, showname):
                self.client = client
                self.name = name
                self.length = length
                self.chance = 1
                self.showname = showname
    class Area:
        """Represents a single instance of an area."""
        def __init__(self,
                     area_id,
                     server,
                     name,
                     background,
                     bg_lock,
                     evidence_mod='FFA',
                     locking_allowed=False,
                     iniswap_allowed=True,
                     showname_changes_allowed=True,
                     shouts_allowed=True,
                     jukebox=False,
                     abbreviation='',
                     non_int_pres_only=False):
            self.iniswap_allowed = iniswap_allowed
            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.name = name
            self.background = background
            self.bg_lock = bg_lock
            self.server = server
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.current_music = ''
            self.current_music_player = ''
            self.current_music_player_ipid = -1
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.evidence_mod = evidence_mod
            self.locking_allowed = locking_allowed
            self.showname_changes_allowed = showname_changes_allowed
            self.shouts_allowed = shouts_allowed
            self.abbreviation = abbreviation
            self.cards = dict()
            """
            #debug
            self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
            self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
            self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
            """

            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.non_int_pres_only = non_int_pres_only
            self.jukebox = jukebox
            self.jukebox_votes = []
            self.jukebox_prev_char_id = -1

            self.owners = []
            self.afkers = []

        class Locked(Enum):
            """Lock state of an area."""
            FREE = 1,
            SPECTATABLE = 2,
            LOCKED = 3

        def new_client(self, client):
            """Add a client to the area."""
            self.clients.add(client)
            self.server.area_manager.send_arup_players()
            if client.char_id != -1:
                database.log_room('area.join', client, self)

        def remove_client(self, client):
            """Remove a disconnected client from the area."""
            self.clients.remove(client)
            if client in self.afkers:
                self.afkers.remove(client)
            if len(self.clients) == 0:
                self.change_status('IDLE')
            if client.char_id != -1:
                database.log_room('area.leave', client, self)

        def unlock(self):
            """Mark the area as unlocked."""
            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.invite_list = {}
            self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is open now.')

        def spectator(self):
            """Mark the area as spectator-only."""
            self.is_locked = self.Locked.SPECTATABLE
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is spectatable now.')

        def lock(self):
            """Mark the area as locked."""
            self.is_locked = self.Locked.LOCKED
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is locked now.')

        def is_char_available(self, char_id):
            """
            Check if a character is available for use.
            :param char_id: character ID
            """
            return char_id not in [x.char_id for x in self.clients]

        def get_rand_avail_char_id(self):
            """Get a random available character ID."""
            avail_set = set(range(len(
                self.server.char_list))) - {x.char_id
                                            for x in self.clients}
            if len(avail_set) == 0:
                raise AreaError('No available characters.')
            return random.choice(tuple(avail_set))

        def send_command(self, cmd, *args):
            """
            Broadcast an AO-compatible command to all clients in the area.
            """
            for c in self.clients:
                c.send_command(cmd, *args)

        def send_owner_command(self, cmd, *args):
            """
            Send an AO-compatible command to all owners of the area
            that are not currently in the area.
            """
            for c in self.owners:
                if c not in self.clients:
                    c.send_command(cmd, *args)

        def broadcast_ooc(self, msg):
            """
            Broadcast an OOC message to all clients in the area.
            :param msg: message
            """
            self.send_command('CT', self.server.config['hostname'], msg, '1')
            self.send_owner_command(
                'CT',
                '[' + self.abbreviation + ']' + self.server.config['hostname'],
                msg, '1')

        def set_next_msg_delay(self, msg_length):
            """
            Set the delay when the next IC message can be send by any client.
            :param msg_length: estimated length of message (ms)
            """
            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def is_iniswap(self, client, preanim, anim, char, sfx):
            """
            Determine if a client is performing an INI swap.
            :param client: client attempting the INI swap.
            :param preanim: name of preanimation
            :param anim: name of idle/talking animation
            :param char: name of character

            """
            if self.iniswap_allowed:
                return False
            if '..' in preanim or '..' in anim or '..' in char:
                # Prohibit relative paths
                return True
            if char.lower() != client.char_name.lower():
                for char_link in self.server.allowed_iniswaps:
                    # Only allow if both the original character and the
                    # target character are in the allowed INI swap list
                    if client.char_name in char_link and char in char_link:
                        return False
            return not self.server.char_emotes[char].validate(
                preanim, anim, sfx)

        def add_jukebox_vote(self, client, music_name, length=-1, showname=''):
            """
            Cast a vote on the jukebox.
            :param music_name: track name
            :param length: length of track (Default value = -1)
            :param showname: showname of voter (?) (Default value = '')
            """
            if not self.jukebox:
                return
            if length <= 0:
                self.remove_jukebox_vote(client, False)
            else:
                self.remove_jukebox_vote(client, True)
                self.jukebox_votes.append(
                    self.JukeboxVote(client, music_name, length, showname))
                client.send_ooc('Your song was added to the jukebox.')
                if len(self.jukebox_votes) == 1:
                    self.start_jukebox()

        def remove_jukebox_vote(self, client, silent):
            """
            Removes a vote on the jukebox.
            :param client: client whose vote should be removed
            :param silent: do not notify client

            """
            if not self.jukebox:
                return
            for current_vote in self.jukebox_votes:
                if current_vote.client.id == client.id:
                    self.jukebox_votes.remove(current_vote)
            if not silent:
                client.send_ooc('You removed your song from the jukebox.')

        def get_jukebox_picked(self):
            """Randomly choose a track from the jukebox."""
            if not self.jukebox:
                return
            if len(self.jukebox_votes) == 0:
                return None
            elif len(self.jukebox_votes) == 1:
                return self.jukebox_votes[0]
            else:
                weighted_votes = []
                for current_vote in self.jukebox_votes:
                    i = 0
                    while i < current_vote.chance:
                        weighted_votes.append(current_vote)
                        i += 1
                return random.choice(weighted_votes)

        def start_jukebox(self):
            """Initialize jukebox mode if needed and play the next track."""
            # There is a probability that the jukebox feature has been turned off since then,
            # we should check that.
            # We also do a check if we were the last to play a song, just in case.
            if not self.jukebox:
                if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID':
                    self.current_music = ''
                return

            vote_picked = self.get_jukebox_picked()

            if vote_picked is None:
                self.current_music = ''
                return

            if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(
                    self.jukebox_votes) > 1:
                self.jukebox_prev_char_id = vote_picked.client.char_id
                if vote_picked.showname == '':
                    self.send_command('MC', vote_picked.name,
                                      vote_picked.client.char_id)
                else:
                    self.send_command('MC', vote_picked.name,
                                      vote_picked.client.char_id,
                                      vote_picked.showname)
            else:
                self.send_command('MC', vote_picked.name, -1)

            self.current_music_player = 'The Jukebox'
            self.current_music_player_ipid = 'has no IPID'
            self.current_music = vote_picked.name

            for current_vote in self.jukebox_votes:
                # Choosing the same song will get your votes down to 0, too.
                # Don't want the same song twice in a row!
                if current_vote.name == vote_picked.name:
                    current_vote.chance = 0
                else:
                    current_vote.chance += 1

            if self.music_looper:
                self.music_looper.cancel()
            self.music_looper = asyncio.get_event_loop().call_later(
                vote_picked.length, lambda: self.start_jukebox())

        def play_music(self, name, cid, loop=0, showname="", effects=0):
            """
            Play a track.
            :param name: track name
            :param cid: origin character ID
            :param loop: 1 for clientside looping, 0 for no looping (2.8)
            :param showname: showname of origin user
            :param effects: fade out/fade in/sync/etc. effect bitflags
            """
            # If it's anything other than 0, it's looping. (Legacy music.yaml support)
            if loop != 0:
                loop = 1
            self.send_command('MC', name, cid, showname, loop, 0, effects)

        def can_send_message(self, client):
            """
            Check if a client can send an IC message in this area.
            :param client: sender
            """
            if self.cannot_ic_interact(client):
                client.send_ooc('This is a locked area - ask the CM to speak.')
                return False
            return (time.time() * 1000.0 - self.next_message_time) > 0

        def cannot_ic_interact(self, client):
            """
            Check if this room is locked to a client.
            :param client: sender
            """
            return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list

        def change_hp(self, side, val):
            """
            Set the penalty bars.
            :param side: 1 for defense; 2 for prosecution
            :param val: value from 0 to 10
            """
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val
            self.send_command('HP', side, val)

        def change_background(self, bg):
            """
            Set the background.
            :param bg: background name
            :raises: AreaError if `bg` is not in background list
            """
            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError('Invalid background name.')
            self.background = bg
            self.send_command('BN', self.background)

        def change_status(self, value):
            """
            Set the status of the room.
            :param value: status code
            """
            allowed_values = ('idle', 'rp', 'casing', 'looking-for-players',
                              'lfp', 'recess', 'gaming')
            if value.lower() not in allowed_values:
                raise AreaError(
                    f'Invalid status. Possible values: {", ".join(allowed_values)}'
                )
            if value.lower() == 'lfp':
                value = 'looking-for-players'
            self.status = value.upper()
            self.server.area_manager.send_arup_status()

        def change_doc(self, doc='No document.'):
            """
            Set the doc link.
            :param doc: doc link (Default value = 'No document.')
            """
            self.doc = doc

        def add_to_judgelog(self, client, msg):
            """
            Append an event to the judge log (max 10 items).
            :param client: event origin
            :param msg: event message
            """
            if len(self.judgelog) >= 10:
                self.judgelog = self.judgelog[1:]
            self.judgelog.append(f'{client.char_name} ({client.ip}) {msg}.')

        def add_music_playing(self, client, name, showname=''):
            """
            Set info about the current track playing.
            :param client: player
            :param showname: showname of player (can be blank)
            :param name: track name
            """
            if showname != '':
                self.current_music_player = f'{showname} ({client.char_name})'
            else:
                self.current_music_player = client.char_name
            self.current_music_player_ipid = client.ipid
            self.current_music = name

        def get_evidence_list(self, client):
            """
            Get the evidence list of the area.
            :param client: requester
            """
            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
            Broadcast an updated evidence list.
            LE#<name>&<desc>&<img>#<name>
            """
            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))

        def get_cms(self):
            """
            Get a list of CMs.
            :return: message
            """
            msg = ''
            for i in self.owners:
                msg += f'[{str(i.id)}] {i.char_name}, '
            if len(msg) > 2:
                msg = msg[:-2]
            return msg

        class JukeboxVote:
            """Represents a single vote cast for the jukebox."""
            def __init__(self, client, name, length, showname):
                self.client = client
                self.name = name
                self.length = length
                self.chance = 1
                self.showname = showname
示例#4
0
    class Area:
        def __init__(self,
                     area_id,
                     server,
                     name,
                     background,
                     bg_lock,
                     evidence_mod='FFA',
                     locking_allowed=False,
                     iniswap_allowed=True,
                     rp_getarea_allowed=True,
                     rp_getareas_allowed=True):
            self.iniswap_allowed = iniswap_allowed
            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.name = name
            self.background = background
            self.bg_lock = bg_lock
            self.server = server
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.current_music = ''
            self.current_music_player = ''
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.evidence_mod = evidence_mod
            self.locking_allowed = locking_allowed
            #New lines
            self.rp_getarea_allowed = rp_getarea_allowed
            self.rp_getareas_allowed = rp_getareas_allowed
            self.owned = False
            """
            #debug
            self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
            self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
            self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
            """

            self.is_locked = False
            self.is_gmlocked = False
            self.is_modlocked = False

        def new_client(self, client):
            self.clients.add(client)

        def remove_client(self, client):
            self.clients.remove(client)
            if len(self.clients) == 0:
                self.unlock()
            if client.is_cm:
                client.is_cm = False
                self.owned = False
                if self.is_locked:
                    self.unlock()

        def unlock(self):
            self.is_locked = False
            if not self.is_gmlocked and not self.is_modlocked:
                self.invite_list = {}

        def gmunlock(self):
            self.is_gmlocked = False
            self.is_locked = False
            if not self.is_modlocked:
                self.invite_list = {}

        def modunlock(self):
            self.is_modlocked = False
            self.is_gmlocked = False
            self.is_locked = False
            self.invite_list = {}

        def is_char_available(self, char_id):
            return char_id not in [x.char_id for x in self.clients]

        def get_rand_avail_char_id(self):
            avail_set = set(range(len(self.server.char_list))) - set(
                [x.char_id for x in self.clients])
            if len(avail_set) == 0:
                raise AreaError('No available characters.')
            return random.choice(tuple(avail_set))

        def send_command(self, cmd, *args):
            for c in self.clients:
                c.send_command(cmd, *args)

        def send_host_message(self, msg):
            self.send_command('CT', self.server.config['hostname'], msg)

        def set_next_msg_delay(self, msg_length):
            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def is_iniswap(self, client, anim1, anim2, char):
            if self.iniswap_allowed:
                return False
            if '..' in anim1 or '..' in anim2:
                return True
            for char_link in self.server.allowed_iniswaps:
                if client.get_char_name() in char_link and char in char_link:
                    return False
            return True

        def play_music(self, name, cid, length=-1):
            self.send_command('MC', name, cid)
            if self.music_looper:
                self.music_looper.cancel()
            if length > 0:
                self.music_looper = asyncio.get_event_loop().call_later(
                    length, lambda: self.play_music(name, -1, length))

        def can_send_message(self):
            return (time.time() * 1000.0 - self.next_message_time) > 0

        def change_hp(self, side, val):
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val
            self.send_command('HP', side, val)

        def change_background(self, bg):
            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError('Invalid background name.')
            self.background = bg
            self.send_command('BN', self.background)

        def change_background_mod(self, bg):
            self.background = bg
            self.send_command('BN', self.background)

        def change_status(self, value):
            allowed_values = ('idle', 'building-open', 'building-full',
                              'casing-open', 'casing-full', 'recess')
            if value.lower() not in allowed_values:
                raise AreaError('Invalid status. Possible values: {}'.format(
                    ', '.join(allowed_values)))
            self.status = value.upper()

        def change_doc(self, doc='No document.'):
            self.doc = doc

        def add_to_judgelog(self, client, msg):
            if len(self.judgelog) >= 10:
                self.judgelog = self.judgelog[1:]
            self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(),
                                                      client.get_ip(), msg))

        def add_music_playing(self, client, name):
            self.current_music_player = client.get_char_name()
            self.current_music = name

        def get_evidence_list(self, client):
            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
                LE#<name>&<desc>&<img>#<name>

            """
            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))
示例#5
0
    class Area:
        """Represents a single instance of an area."""
        def __init__(self,
                     area_id,
                     server,
                     name,
                     background,
                     bg_lock,
                     evidence_mod='FFA',
                     locking_allowed=False,
                     iniswap_allowed=True,
                     showname_changes_allowed=True,
                     shouts_allowed=True,
                     jukebox=False,
                     abbreviation='',
                     non_int_pres_only=False):
            self.iniswap_allowed = iniswap_allowed
            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.name = name
            self.background = background
            self.bg_lock = bg_lock
            self.server = server
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.current_music = ''
            self.current_music_player = ''
            self.current_music_player_ipid = -1
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.evidence_mod = evidence_mod
            self.locking_allowed = locking_allowed
            self.showname_changes_allowed = showname_changes_allowed
            self.shouts_allowed = shouts_allowed
            self.abbreviation = abbreviation
            self.cards = dict()
            """
            #debug
            self.evidence_list.append(Evidence("WOW", "desc", "1.png"))
            self.evidence_list.append(Evidence("wewz", "desc2", "2.png"))
            self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png"))
            """

            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.non_int_pres_only = non_int_pres_only
            self.jukebox = jukebox
            self.jukebox_votes = []
            self.jukebox_prev_char_id = -1

            self.owners = []
            self.afkers = []
            self.last_ic_message = None
            
            # Testimony stuff
            self.is_testifying = False
            self.is_examining = False
            self.testimony_limit = self.server.config['testimony_limit'] + 1
            self.testimony = self.Testimony('N/A', self.testimony_limit)
            self.examine_index = 0


        class Locked(Enum):
            """Lock state of an area."""
            FREE = 1,
            SPECTATABLE = 2,
            LOCKED = 3

        def new_client(self, client: ClientManager.Client):
            """Add a client to the area."""
            self.clients.add(client)
            self.server.area_manager.send_arup_players()
            if client.char_id != -1:
                database.log_room('area.join', client, self)

        def remove_client(self, client: ClientManager.Client):
            """Remove a disconnected client from the area.
            Args:
                client (ClientManager.Client): Client to remove
            """

            self.clients.remove(client)
            if client in self.afkers:
                self.afkers.remove(client)
            if len(self.clients) == 0:
                self.change_status('IDLE')
            if client.char_id != -1:
                database.log_room('area.leave', client, self)

        def client_can_additive(self, client: ClientManager.Client):
            if self.last_ic_message is None:
                return False

            last_char_id = self.last_ic_message[8]
            if client.char_id == last_char_id:
                return True
            return False

        def unlock(self):
            """Mark the area as unlocked."""
            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.invite_list = {}
            self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is open now.')

        def spectator(self):
            """Mark the area as spectator-only."""
            self.is_locked = self.Locked.SPECTATABLE
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is spectatable now.')

        def lock(self):
            """Mark the area as locked."""
            self.is_locked = self.Locked.LOCKED
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is locked now.')

        def is_char_available(self, char_id: int) -> bool:
            """Check if a character is available for use.
            Args:
                char_id (int): character ID
            Returns:
                bool: True if the character is available. False if not available
            """

            return char_id not in [x.char_id for x in self.clients]

        def get_rand_avail_char_id(self):
            """Get a random available character ID."""
            avail_set = set(range(len(
                self.server.char_list))) - {x.char_id
                                            for x in self.clients}
            if len(avail_set) == 0:
                raise AreaError('No available characters.')
            return random.choice(tuple(avail_set))

        def send_command(self, cmd: str, *args):
            """Broadcast an AO-compatible command to all clients in the area.
            Args:
                cmd (str): Command to send
            """

            for c in self.clients:
                c.send_command(cmd, *args)

        def send_owner_command(self, cmd: str, *args):
            """Send an AO-compatible command to all owners of the area
            that are not currently in the area.
            Args:
                cmd (str): Command to send
            """
            for c in self.owners:
                if c not in self.clients:
                    c.send_command(cmd, *args)

        def broadcast_ooc(self, msg: str):
            """Broadcast an OOC message to all clients in the area.
            Args:
                msg (str): message to be broadcasted
            """

            self.send_command('CT', self.server.config['hostname'], msg, '1')
            self.send_owner_command(
                'CT',
                '[' + self.abbreviation + ']' + self.server.config['hostname'],
                msg, '1')

        def set_next_msg_delay(self, msg_length: int):
            """Set the delay when the next IC message can be send by any client.
            Args:
                msg_length (int): estimated length of message (ms)
            """

            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def is_iniswap(self, client: ClientManager.Client, preanim: str, anim: str, char: str, sfx) -> bool:
            """Determine if a client is performing an INI swap.
            Args:
                client (ClientManager.Client): client attempting the INI swap.
                preanim (str): name of preanimation
                anim (str): name of idle/talking animation
                char (str): name of character
                sfx ([type]): [description]
            Returns:
                bool: True if client is ini_swap, false if client is not
            """

            if self.iniswap_allowed:
                return False
            if '..' in preanim or '..' in anim or '..' in char:
                # Prohibit relative paths
                return True
            if char.lower() != client.char_name.lower():
                for char_link in self.server.allowed_iniswaps:
                    # Only allow if both the original character and the
                    # target character are in the allowed INI swap list
                    if client.char_name in char_link and char in char_link:
                        return False
            return not self.server.char_emotes[char].validate(preanim, anim, sfx)

        def add_jukebox_vote(self, client: ClientManager.Client, music_name: str, length: int = -1, showname: str = ''):
            """Cast a vote on the jukebox.
            Args:
                client (ClientManager.Client): Client that is requesting
                music_name (str): track name
                length (int, optional): length of track. Defaults to -1.
                showname (str, optional): showname of voter. Defaults to ''.
            """
            if not self.jukebox:
                return
            if length <= 0:
                self.remove_jukebox_vote(client, False)
            else:
                self.remove_jukebox_vote(client, True)
                self.jukebox_votes.append(
                    self.JukeboxVote(client, music_name, length, showname))
                client.send_ooc('Your song was added to the jukebox.')
                if len(self.jukebox_votes) == 1:
                    self.start_jukebox()

        def remove_jukebox_vote(self, client: ClientManager.Client, silent: bool):
            """Removes a vote on the jukebox.
            Args:
                client (ClientManager.Client): client whose vote should be removed
                silent (bool): do not notify client
            """

            if not self.jukebox:
                return
            for current_vote in self.jukebox_votes:
                if current_vote.client.id == client.id:
                    self.jukebox_votes.remove(current_vote)
            if not silent:
                client.send_ooc(
                    'You removed your song from the jukebox.')

        def get_jukebox_picked(self):
            """Randomly choose a track from the jukebox."""
            if not self.jukebox:
                return
            if len(self.jukebox_votes) == 0:
                return None
            elif len(self.jukebox_votes) == 1:
                return self.jukebox_votes[0]
            else:
                weighted_votes = []
                for current_vote in self.jukebox_votes:
                    i = 0
                    while i < current_vote.chance:
                        weighted_votes.append(current_vote)
                        i += 1
                return random.choice(weighted_votes)

        def start_jukebox(self):
            """Initialize jukebox mode if needed and play the next track."""
            # There is a probability that the jukebox feature has been turned off since then,
            # we should check that.
            # We also do a check if we were the last to play a song, just in case.
            if not self.jukebox:
                if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID':
                    self.current_music = ''
                return

            vote_picked = self.get_jukebox_picked()

            if vote_picked is None:
                self.current_music = ''
                return

            if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(
                    self.jukebox_votes) > 1:
                self.jukebox_prev_char_id = vote_picked.client.char_id
                if vote_picked.showname == '':
                    self.send_command('MC', vote_picked.name,
                                      vote_picked.client.char_id)
                else:
                    self.send_command('MC', vote_picked.name,
                                      vote_picked.client.char_id,
                                      vote_picked.showname)
            else:
                self.send_command('MC', vote_picked.name, -1)

            self.current_music_player = 'The Jukebox'
            self.current_music_player_ipid = 'has no IPID'
            self.current_music = vote_picked.name

            for current_vote in self.jukebox_votes:
                # Choosing the same song will get your votes down to 0, too.
                # Don't want the same song twice in a row!
                if current_vote.name == vote_picked.name:
                    current_vote.chance = 0
                else:
                    current_vote.chance += 1

            if self.music_looper:
                self.music_looper.cancel()
            self.music_looper = asyncio.get_event_loop().call_later(
                vote_picked.length, lambda: self.start_jukebox())

        def play_music(self, name: str, cid: int, loop: int = 0, showname: str ="", effects: int = 0):
            """Play a track.
            Args:
                name (str): track name
                cid (int): origin character ID
                loop (int, optional): 1 for clientside looping, 0 for no looping (2.8). Defaults to 0.
                showname (str, optional): showname of origin user. Defaults to "".
                effects (int, optional): fade out/fade in/sync/etc. effect bitflags. Defaults to 0.
            """

            # If it's anything other than 0, it's looping. (Legacy music.yaml support)
            if loop != 0:
                loop = 1
            self.send_command('MC', name, cid, showname, loop, 0, effects)

        def can_send_message(self, client: ClientManager.Client) -> bool:
            """Check if a client can send an IC message in this area.
            Args:
                client (ClientManager.Client): sender
            Returns:
                bool: True is client can send a message, False if not
            """
            
            if self.cannot_ic_interact(client):
                client.send_ooc(
                    'This is a locked area - ask the CM to speak.')
                return False
            return (time.time() * 1000.0 - self.next_message_time) > 0

        def cannot_ic_interact(self, client: ClientManager.Client) -> bool:
            """Check if this room is locked to a client.
            Args:
                client (ClientManager.Client): sender
            Returns:
                bool: True if the client cannot interact, False otherwise
            """     
            return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list

        def change_hp(self, side: int, val: int):
            """Set the penalty bars.
            Args:
                side (int): 1 for defense; 2 for prosecution
                val (int): value from 0 to 10
            Raises:
                AreaError: If side is not between 1-2 inclusive or val is not between 0-10
            """
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val
            self.send_command('HP', side, val)

        def change_background(self, bg: str):
            """ Set the background.            
            Args:
                bg (str): background name
            Raises:
                AreaError: if `bg` is not in background list
            """

            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError('Invalid background name.')
            self.background = bg
            self.send_command('BN', self.background)

        def change_status(self, value: str):
            """Set the status of the room.
            Args:
                value (str): status code
            Raises:
                AreaError: If the value is not a valid status code
            """

            allowed_values = ('idle', 'rp', 'casing', 'looking-for-players',
                              'lfp', 'recess', 'gaming')
            if value.lower() not in allowed_values:
                raise AreaError(
                    f'Invalid status. Possible values: {", ".join(allowed_values)}'
                )
            if value.lower() == 'lfp':
                value = 'looking-for-players'
            self.status = value.upper()
            self.server.area_manager.send_arup_status()

        def change_doc(self, doc='No document.'):
            """Set the doc link.
            Args:
                doc (str, optional): doc link. Defaults to 'No document.'.
            """
            self.doc = doc

        def add_to_judgelog(self, client: ClientManager.Client, msg: str):
            """Append an event to the judge log (max 10 items).
            Args:
                client (ClientManager.Client): event origin
                msg (str): event message
            """
            
            if len(self.judgelog) >= 10:
                self.judgelog = self.judgelog[1:]
            self.judgelog.append(
                f'{client.char_name} ({client.ip}) {msg}.')

        def add_music_playing(self, client: ClientManager.Client, name: str, showname: str = ''):
            """Set info about the current track playing.
            Args:
                client (ClientManager.Client): player
                name (str): showname of player (can be blank)
                showname (str, optional): track name. Defaults to ''.
            """

            if showname != '':
                self.current_music_player = f'{showname} ({client.char_name})'
            else:
                self.current_music_player = client.char_name
            self.current_music_player_ipid = client.ipid
            self.current_music = name

        def get_evidence_list(self, client: ClientManager.Client) -> List[EvidenceList.Evidence]:
            """Get the evidence list of the area.
            Args:
                client (ClientManager.Client): requester
            Returns:
                List[EvidenceList.Evidence]: A list containing Evidence
            """
            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
            Broadcast an updated evidence list.
            LE#<name>&<desc>&<img>#<name>
            """
            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))

        def get_cms(self) -> str:
            """Get a list of CMs.
            Returns:
                str: String of CM's comma separated
            """
            
            msg = ''
            for i in self.owners:
                msg += f'[{str(i.id)}] {i.char_name}, '
            if len(msg) > 2:
                msg = msg[:-2]
            return msg

        class Testimony:
            """Represents a complete group of statements to be pressed or objected to."""
            
            def __init__(self, title: str, limit: int):
                self.title = title
                self.statements = []
                self.limit = limit
            
            def add_statement(self, message: tuple) -> bool:
                """Add a statement.
                Args:
                    message (tuple): the IC message to add
                Returns:
                    bool: whether the message was added
                """
                message = message[:14] + (1,) + message[15:]
                if len(self.statements) >= self.limit:
                    return False
                self.statements.append(message)
                return True
            
            def remove_statement(self, index: int) -> bool:
                """Remove a statement.
                Args:
                    index (int): index of the statement to remove
                Returns:
                    bool: whether the statement was removed
                """
                if index < 1 or index > len(self.statements) + 1:
                    return False
                i = 0
                while i < len(self.statements):
                    if i == index:
                        self.statements.remove(self.statements[i])
                        return True
                    i += 1
                return False # shouldn't happen
                        
            def amend_statement(self, index: int, message: list) -> bool:
                """Amend a statement.
                Args:
                    index (int): index of the statement to amend
                    message (list): the new statement
                Returns:
                    bool: whether the statement was amended
                """
                if index < 1 or index > len(self.statements) + 1:
                    return False
                message[14] = 1 # message[14] is color, and 1 is (by default) green
                message = tuple(message)
                i = 0
                while i < len(self.statements):
                    if i == index:
                        self.statements[i] = message
                        return True
                    i += 1
                return True
            
        def start_testimony(self, client: ClientManager.Client, title: str) -> bool:
            """
            Start a new testimony in this area.
            Args:
                client (ClientManager.Client): requester
                title (str): title of the testimony
            Returns:
                bool: whether the testimony was started
            """
            if client not in self.owners and (self.evidence_mod == "HiddenCM" or self.evidence_mod == "Mods"):
                # some servers don't utilise area owners, so we use evidence_mod to determine behavior
                client.send_ooc('You don\'t have permission to start a new testimony in this area!')
                return False
            elif self.is_testifying:
                client.send_ooc('You can\'t start a new testimony until you finish this one!')
                return False
            elif self.is_examining:
                client.send_ooc('You can\'t start a new testimony during an examination!')
                return False
            elif title == '':
                client.send_ooc('You can\'t start a new testimony without a title!')
                return False
            self.testimony = self.Testimony(title, self.testimony_limit)
            self.broadcast_ooc('Began testimony: ' + title)
            self.is_testifying = True
            self.send_command('RT', 'testimony1')
            return True

        def start_examination(self, client: ClientManager.Client) -> bool:
            """
            Start an examination of this area's testimony.
            Args:
                client (ClientManager.Client): requester
            Returns:
                bool: whether the examination was started
            """
            if client not in self.owners and (self.evidence_mod == "HiddenCM" or self.evidence_mod == "Mods"):
                client.send_ooc('You don\'t have permission to start a new examination in this area!')
                return False
            elif self.is_testifying:
                client.send_ooc('You can\'t start an examination during a testimony! (Hint: Say \'/end\' to stop recording!)')
                return False
            elif self.is_examining:
                client.send_ooc('You can\'t start an examination until you finish this one!')
                return False
            self.examine_index = 0
            self.is_examining = True
            self.send_command('RT', 'testimony2')
            return True
        
        def end_testimony(self, client: ClientManager.Client) -> bool:
            """
            End the current testimony or examination.
            Args:
                client (ClientManager.Client): requester
            Returns:
                bool: if the current testimony or examination was ended
            """
            if client not in self.owners and (self.evidence_mod == "HiddenCM" or self.evidence_mod == "Mods"):
                client.send_ooc('You don\'t have permission to end testimonies or examinations in this area!')
                return False
            elif self.is_testifying:
                if len(self.testimony.statements) <= 1:
                    client.send_ooc('Please add at least one statement before ending your testimony.')
                    return False
                self.is_testifying = False
                self.broadcast_ooc('Recording stopped.')
                return True
            elif self.is_examining:
                self.is_examining = False
                self.broadcast_ooc('Examination stopped.')
                return True
            else:
                client.send_ooc('No testimony or examination in progress.')
                return False
            
        def amend_testimony(self, client: ClientManager.Client, index:int, statement: list) -> bool:
            """
            Replace the statement at <index> with a new <statement>.
            Args:
                client (ClientManager.Client): requester
                index (int): index of the statement to amend
                statement (list): the new statement
            Returns:
                bool: whether the statement was amended
            """
            if client not in self.owners and (self.evidence_mod == "HiddenCM" or self.evidence_mod == "Mods"):
                client.send_ooc('You don\'t have permission to amend testimony in this area!')
                return False
            if self.testimony.amend_statement(index, statement):
                client.send_ooc('Amended statement ' + str(index) + ' successfully.')
                return True
            else:
                client.send_ooc('Couldn\'t amend statement ' + str(index) + '. Are you sure it exists?')
                return False
            
        def remove_statement(self, client: ClientManager.Client, index: int) -> bool:
            """
            Remove the statement at <index>.
            Args:
                client (ClientManager.Client): requester
                index (int): index of the statement to remove
            Returns:
                bool: whether the statement was removed
            """
            if client not in self.owners and (self.evidence_mod == "HiddenCM" or self.evidence_mod == "Mods"):
                client.send_ooc('You don\'t have permission to amend testimony in this area!')
                return False
            if self.testimony.remove_statement(index):
                client.send_ooc('Removed statement ' + str(index) + ' successfully.')
                return True
            else:
                client.send_ooc('Couldn\'t remove statement ' + str(index) + '. Are you sure it exists?')
                return True
            
        def navigate_testimony(self, client: ClientManager.Client, command: str, index: int = None) -> bool:
            """
            Navigate the current testimony using the commands >, <, =, and [>|<]<index>.
            Args:
                client (ClientManager.Client): requester
                command (str): either >, <, or =
                index (int): index of the statement to move to, or None
            Returns:
                bool: if the navigation was successful
            """
            if len(self.testimony.statements) <= 1:
                client.send_ooc('Testimony is empty, can\'t navigate!') # should never happen
                return False
            if index == None:
                if command == '=':
                    if self.examine_index == 0:
                        self.examine_index = 1
                elif command == '>':
                    if len(self.testimony.statements) <= self.examine_index + 1:
                        self.broadcast_ooc('Reached end of testimony, looping...')
                        self.examine_index = 1
                    else:
                        self.examine_index = self.examine_index + 1
                elif command == '<':
                    if self.examine_index <= 1:
                        client.send_ooc('Can\'t go back, already on the first statement!')
                        return False
                    else:
                        self.examine_index = self.examine_index - 1
            else:
                try:
                    self.examine_index = int(index)
                except ValueError:
                    client.send_ooc("That does not look like a valid statement number!")
                    return False
            self.send_command('MS', *self.testimony.statements[self.examine_index])
            return True
                
        class JukeboxVote:
            """Represents a single vote cast for the jukebox."""
            def __init__(self, client, name, length, showname):
                self.client = client
                self.name = name
                self.length = length
                self.chance = 1
                self.showname = showname
示例#6
0
    class Area:
        """Represents a single instance of an area."""
        def __init__(self,
                     area_id,
                     server,
                     name,
                     background,
                     bg_lock=False,
                     evidence_mod='FFA',
                     locking_allowed=False,
                     iniswap_allowed=True,
                     showname_changes_allowed=True,
                     shouts_allowed=True,
                     jukebox=False,
                     abbreviation='',
                     non_int_pres_only=False,
                     is_hub=False,
                     hubid=0,
                     hubtype='default',
                     desc=''):
            self.timetomove = 0
            self.desc = ''
            self.iniswap_allowed = iniswap_allowed
            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.name = name
            self.background = background
            self.bg_lock = bg_lock
            self.server = server
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.evi_list = EvidenceList()
            self.is_restricted = False
            self.connections = []
            self.evidence_mod = evidence_mod
            self.locking_allowed = locking_allowed
            self.showname_changes_allowed = showname_changes_allowed
            self.shouts_allowed = shouts_allowed
            self.abbreviation = abbreviation
            self.cards = dict()
            self.hidden = False
            self.password = ''
            self.poslock = []
            self.last_speaker = None
            self.last_ooc = ''
            self.spies = set()
            self.webblock = False
            self.timers = [AreaManager.Timer() for _ in range(4)]

            # Hub stuff
            self.is_hub = is_hub
            self.hubid = hubid
            self.hubtype = hubtype
            self.hub = None
            self.subareas = []
            self.sub = False
            self.cur_subid = 1

            # Music stuff
            self.allowmusic = True
            self.loop = False
            self.current_music = ''
            self.current_music_player = ''
            self.current_music_player_ipid = -1
            self.music_looper = None
            self.ambiance = ''
            self.cmusic_list = []
            self.cmusic_listname = ''

            #Testimony stuff
            self.is_recording = False
            self.recorded_messages = []
            self.statement = 0

            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.non_int_pres_only = non_int_pres_only
            self.jukebox = jukebox
            self.jukebox_votes = []
            self.jukebox_prev_char_id = -1

            self.owners = []

        class Locked(Enum):
            """Lock state of an area."""
            FREE = 1,
            SPECTATABLE = 2,
            LOCKED = 3

        def new_client(self, client):
            """Add a client to the area."""
            self.clients.add(client)
            lobby = self.server.area_manager.default_area()
            if self == lobby:
                for area in self.server.area_manager.areas:
                    if area.is_hub:
                        area.sub_arup_players()
                        for sub in area.subareas:
                            if sub.is_restricted and len(sub.clients) > 0:
                                sub.conn_arup_players()
            if client.char_id != -1:
                database.log_room('area.join', client, self)
                if client.ambiance != self.ambiance:
                    client.ambiance = self.ambiance
                    client.send_command(
                        "MC",
                        self.ambiance,
                        -1,
                        "",
                        1,
                        1,
                        int(MusicEffect.FADE_OUT),
                    )
                if self.loop:
                    client.send_command(
                        "MC",
                        'None',
                        -1,
                        "",
                        0,
                        0,
                        int(MusicEffect.FADE_OUT),
                    )
                    client.current_music = self.current_music
                else:
                    if client.current_music != self.current_music:
                        client.send_command(
                            "MC",
                            self.current_music,
                            -1,
                            "",
                            1,
                            0,
                            int(MusicEffect.FADE_OUT),
                        )
                        client.current_music = self.current_music

            if self.desc != '':
                client.send_ooc(self.desc)

            # Update the timers
            timer = self.server.area_manager.timer
            if timer.set:
                s = int(not timer.started)
                current_time = timer.static
                if timer.started:
                    current_time = timer.target - arrow.get()
                int_time = int(current_time.total_seconds()) * 1000
                # Unhide the timer
                client.send_command('TI', 0, 2)
                # Start the timer
                client.send_command('TI', 0, s, int_time)
            else:
                # Stop the timer
                client.send_command('TI', 0, 3, 0)
                # Hide the timer
                client.send_command('TI', 0, 1)

            for timer_id, timer in enumerate(self.timers):
                # Send static time if applicable
                if timer.set:
                    s = int(not timer.started)
                    current_time = timer.static
                    if timer.started:
                        current_time = timer.target - arrow.get()
                    int_time = int(current_time.total_seconds()) * 1000
                    # Start the timer
                    client.send_command('TI', timer_id + 1, s, int_time)
                    # Unhide the timer
                    client.send_command('TI', timer_id + 1, 2)
                    client.send_ooc(f'Timer {timer_id+1} is at {current_time}')
                else:
                    # Stop the timer
                    client.send_command('TI', timer_id + 1, 1, 0)
                    # Hide the timer
                    client.send_command('TI', timer_id + 1, 3)

        def remove_client(self, client):
            """Remove a disconnected client from the area."""
            self.clients.remove(client)
            if self.sub:
                for othersub in self.hub.subareas:
                    if othersub.is_restricted:
                        if self in othersub.connections:
                            othersub.conn_arup_players()
            elif self.is_hub:
                for sub in self.subareas:
                    if sub.is_restricted:
                        sub.conn_arup_players()
            if len(self.clients) == 0:
                if len(self.owners) == 0 and not self.is_hub:
                    self.change_status('IDLE')
            if client.char_id != -1:
                database.log_room('area.leave', client, self)

        def unlock(self):
            """Mark the area as unlocked."""
            self.is_locked = self.Locked.FREE
            self.blankposting_allowed = True
            self.invite_list = {}
            if self.sub:
                for othersub in self.hub.subareas:
                    if othersub.is_restricted:
                        if self in othersub.connections:
                            othersub.conn_arup_lock()
                else:
                    self.hub.sub_arup_lock()
            elif self.is_hub:
                self.sub_arup_lock()
                self.server.area_manager.send_arup_lock()
            else:
                self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is open now.')

        def lock(self):
            """Mark the area as locked."""
            self.is_locked = self.Locked.LOCKED
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            if self.sub:
                for othersub in self.hub.subareas:
                    if othersub.is_restricted:
                        if self in othersub.connections:
                            othersub.conn_arup_lock()
                else:
                    self.hub.sub_arup_lock()
            elif self.is_hub:
                self.sub_arup_lock()
                self.server.area_manager.send_arup_lock()
            else:
                self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is locked now.')

        def spectator(self):
            """Mark the area as spectator-only."""
            self.is_locked = self.Locked.SPECTATABLE
            for i in self.clients:
                self.invite_list[i.id] = None
            for i in self.owners:
                self.invite_list[i.id] = None
            if self.sub:
                if self.is_restricted:
                    self.conn_arup_lock()
                else:
                    self.hub.sub_arup_lock()
            elif self.is_hub:
                self.sub_arup_lock()
                self.server.area_manager.send_arup_lock()
            else:
                self.server.area_manager.send_arup_lock()
            self.broadcast_ooc('This area is spectatable now.')

        def is_char_available(self, char_id):
            """
			Check if a character is available for use.
			:param char_id: character ID
			"""
            return char_id not in [x.char_id for x in self.clients]

        def get_rand_avail_char_id(self):
            """Get a random available character ID."""
            avail_set = set(range(len(
                self.server.char_list))) - {x.char_id
                                            for x in self.clients}
            if len(avail_set) == 0:
                raise AreaError('No available characters.')
            return random.choice(tuple(avail_set))

        def send_command(self, cmd, *args):
            """
			Broadcast an AO-compatible command to all clients in the area.
			"""
            for c in self.clients:
                c.send_command(cmd, *args)

        def send_owner_command(self, cmd, *args):
            """
			Send an AO-compatible command to all owners of the area
			that are not currently in the area.
			"""
            for c in self.owners:
                if c not in self.clients and c.listen:
                    c.send_command(cmd, *args)
            for spy in self.spies:
                if spy not in self.clients and spy not in self.owners:
                    spy.send_command(cmd, *args)

        def broadcast_ooc(self, msg):
            """
			Broadcast an OOC message to all clients in the area.
			:param msg: message
			"""
            self.send_command('CT', self.server.config['hostname'], msg, '1')
            self.send_owner_command(
                'CT',
                '[' + self.abbreviation + ']' + self.server.config['hostname'],
                msg, '1')

        def set_next_msg_delay(self, msg_length):
            """
			Set the delay when the next IC message can be send by any client.
			:param msg_length: estimated length of message (ms)
			"""
            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def is_iniswap(self, client, preanim, anim, char, sfx):
            """
			Determine if a client is performing an INI swap.
			:param client: client attempting the INI swap.
			:param preanim: name of preanimation
			:param anim: name of idle/talking animation
			:param char: name of character

			"""
            if self.iniswap_allowed:
                return False
            #if '..' in preanim or '..' in anim or '..' in char:
            # Prohibit relative paths
            #	return True
            if char.lower() != client.char_name.lower():
                for char_link in self.server.allowed_iniswaps:
                    # Only allow if both the original character and the
                    # target character are in the allowed INI swap list
                    if client.char_name in char_link and char in char_link:
                        return False
            return not self.server.char_emotes[char].validate(
                preanim, anim, sfx)

            #if self.music_looper:
            #	self.music_looper.cancel()
            #self.music_looper = asyncio.get_event_loop().call_later(
            #	vote_picked.length, lambda: self.start_jukebox())

        def play_music(self, name, cid, length=0, effects=0):
            """
			Play a track.
			:param name: track name
			:param cid: origin character ID
			:param length: track length (Default value = -1)
			"""
            if self.music_looper:
                self.music_looper.cancel()
            if self.loop or name.startswith('/custom'):
                if length != 0:
                    self.music_looper = asyncio.get_event_loop().call_later(
                        length,
                        lambda: self.play_music(name, -1, length, effects))
                else:
                    length = 1
            else:
                if length != 0:
                    length = 1
            self.send_command('MC', name, cid, '', length, 0, effects)

        def play_msequence(self, file, index=0):
            now = 0
            with open(file, 'r', encoding='utf-8') as chars:
                sequence = yaml.safe_load(chars)
            for item in sequence:
                if index == now:
                    index += 1
                    name = item['name']
                    length = item['length']
                    if self.music_looper:
                        self.music_looper.cancel()
                    self.send_command('MC', name, -1, '', length, 0,
                                      int(MusicEffect.FADE_OUT))
                    self.music_looper = asyncio.get_event_loop().call_later(
                        length, lambda: self.play_msequence(file, index))
                    return
                now += 1
            index = 0
            for item in sequence:
                index += 1
                if item['type'] != 'intro':
                    name = item['name']
                    length = item['length']
                    if self.music_looper:
                        self.music_looper.cancel()
                    self.send_command('MC', name, -1, '', length, 0,
                                      int(MusicEffect.FADE_OUT))
                    self.music_looper = asyncio.get_event_loop().call_later(
                        length, lambda: self.play_msequence(file, index))
                    return
            return

        def play_music_shownamed(self,
                                 name,
                                 cid,
                                 showname,
                                 length=0,
                                 effects=0):
            """
			Play a track, but show showname as the player instead of character
			ID.
			:param name: track name
			:param cid: origin character ID
			:param showname: showname of origin user
			:param length: track length (Default value = -1)
			"""
            if self.music_looper:
                self.music_looper.cancel()
            if self.loop or name.startswith('/custom'):
                if length != 0:
                    self.music_looper = asyncio.get_event_loop().call_later(
                        length,
                        lambda: self.play_music(name, -1, length, effects))
                else:
                    length = 1
            else:
                if length != 0:
                    length = 1
            self.send_command('MC', name, cid, showname, length, 0, effects)

        def music_shuffle(self, arg, client, track=-1):
            """
			Shuffles through tracks randomly, either from entire music list or specific category.
			"""
            arg = arg
            client = client
            if len(arg) != 0:
                index = 0
                for item in self.server.music_list:
                    if item['category'] == arg:
                        for song in item['songs']:
                            index += 1
                if index == 0:
                    client.send_ooc('Category/music not found.')
                    return
                else:
                    music_set = set(range(index))
                    trackid = random.choice(tuple(music_set))
                    while trackid == track:
                        trackid = random.choice(tuple(music_set))
                    index = 0
                    for item in self.server.music_list:
                        if item['category'] == arg:
                            for song in item['songs']:
                                if index == trackid:
                                    self.play_music_shownamed(
                                        song['name'], client.char_id,
                                        '{} Shuffle'.format(arg))
                                    self.music_looper = asyncio.get_event_loop(
                                    ).call_later(
                                        song['length'],
                                        lambda: self.music_shuffle(
                                            arg, client, trackid))
                                    self.add_music_playing(
                                        client, song['name'])
                                    database.log_room('play',
                                                      client,
                                                      self,
                                                      message=song['name'])
                                    return
                                else:
                                    index += 1
            else:
                index = 0
                for item in self.server.music_list:
                    for song in item['songs']:
                        index += 1
                if index == 0:
                    client.send_ooc('Category/music not found.')
                    return
                else:
                    music_set = set(range(index))
                    trackid = random.choice(tuple(music_set))
                    while trackid == track:
                        trackid = random.choice(tuple(music_set))
                    index = 0
                    for item in self.server.music_list:
                        for song in item['songs']:
                            if index == trackid:
                                self.play_music_shownamed(
                                    song['name'], client.char_id,
                                    'Random Shuffle')
                                self.music_looper = asyncio.get_event_loop(
                                ).call_later(
                                    song['length'], lambda: self.music_shuffle(
                                        arg, client, trackid))
                                self.add_music_playing(client, song['name'])
                                database.log_room('play',
                                                  client,
                                                  self,
                                                  message=song['name'])
                                return
                            else:
                                index += 1

        def musiclist_shuffle(self, client, track=-1):
            client = client
            index = 0
            for item in client.area.cmusic_list:
                if 'songs' in item:
                    for song in item['songs']:
                        index += 1
                else:
                    index += 1
            if index == 0:
                client.send_ooc('Area musiclist empty.')
                return
            else:
                music_set = set(range(index))
                trackid = random.choice(tuple(music_set))
                while trackid == track:
                    trackid = random.choice(tuple(music_set))
                index = 0
                for item in client.area.cmusic_list:
                    if 'songs' in item:
                        for song in item['songs']:
                            if index == trackid:
                                if song['length'] <= 5:
                                    client.send_ooc(
                                        'Track seems to have too little or no length, shuffle canceled.'
                                    )
                                    return
                                self.play_music_shownamed(
                                    song['name'], client.char_id,
                                    'Custom Shuffle')
                                self.music_looper = asyncio.get_event_loop(
                                ).call_later(
                                    song['length'],
                                    lambda: self.musiclist_shuffle(
                                        client, trackid))
                                self.add_music_playing(client, song['name'])
                                database.log_room('play',
                                                  client,
                                                  self,
                                                  message=song['name'])
                                return
                            else:
                                index += 1
                    else:
                        if index == trackid:
                            if item['length'] <= 5:
                                client.send_ooc(
                                    'Track seems to have too little or no length, shuffle canceled.'
                                )
                                return
                            self.play_music_shownamed(item['name'],
                                                      client.char_id,
                                                      'Custom Shuffle')
                            self.music_looper = asyncio.get_event_loop(
                            ).call_later(
                                item['length'], lambda: self.musiclist_shuffle(
                                    client, trackid))
                            self.add_music_playing(client, item['name'])
                            database.log_room('play',
                                              client,
                                              self,
                                              message=item['name'])
                            return
                        else:
                            index += 1

        def can_send_message(self, client):
            """
			Check if a client can send an IC message in this area.
			:param client: sender
			"""
            if self.cannot_ic_interact(client):
                client.send_ooc('This is a locked area - ask the CM to speak.')
                return False
            return (time.time() * 1000.0 - self.next_message_time) > 0

        def cannot_ic_interact(self, client):
            """
			Check if this room is locked to a client.
			:param client: sender
			"""
            return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list

        def change_hp(self, side, val):
            """
			Set the penalty bars.
			:param side: 1 for defense; 2 for prosecution
			:param val: value from 0 to 10
			"""
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val
            self.send_command('HP', side, val)

        def change_background(self, bg):
            """
			Set the background.
			:param bg: background name
			:raises: AreaError if `bg` is not in background list
			"""
            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError('Invalid background name.')
            self.background = bg
            self.send_command('BN', self.background)
            if len(self.poslock) > 0:
                self.send_command('SD', '*'.join(client.area.poslock))

        def change_cbackground(self, bg):
            """
			Set the background.
			:param bg: background name
			:raises: AreaError if `bg` is not in background list
			"""
            self.background = bg
            self.send_command('BN', self.background)
            if len(self.poslock) > 0:
                self.send_command('SD', '*'.join(client.area.poslock))

        def change_status(self, value):
            """
			Set the status of the room.
			:param value: status code
			"""
            allowed_values = ('idle', 'rp', 'casing', 'looking-for-players',
                              'lfp', 'recess', 'gaming')
            if value.lower() not in allowed_values:
                raise AreaError(
                    f'Invalid status. Possible values: {", ".join(allowed_values)}'
                )
            if value.lower() == 'lfp':
                value = 'looking-for-players'
            self.status = value.upper()
            if self.sub:
                if self.hub.hubtype == 'arcade' or self.hub.hubtype == 'courtroom':
                    if value == 'looking-for-players':
                        self.hub.status = value.upper()
                    else:
                        lfp = False
                        idle = True
                        recess = True
                        for area in self.hub.subareas:
                            if area.status == 'LOOKING-FOR-PLAYERS':
                                lfp = True
                            if area.status != 'IDLE':
                                idle = False
                            if area.status == 'RP' or area.status == 'CASING' or area.status == 'GAMING':
                                recess = False
                        if lfp == False and not value.lower(
                        ) == 'idle' and not value.lower() == 'recess':
                            self.hub.status = value.upper()
                        if value.lower() == 'idle' and idle == True:
                            self.hub.status = value.upper()
                        if value.lower() == 'recess' and recess == True:
                            self.hub.status = value.upper()
                        if self.hub.status == 'LOOKING-FOR-PLAYERS' and value.lower(
                        ) == 'recess' or self.hub.status == 'LOOKING-FOR-PLAYERS' and value.lower(
                        ) == 'idle':
                            if lfp == False:
                                for area in self.hub.subareas:
                                    if area.status == 'CASING':
                                        self.hub.status = 'CASING'
                                        break
                                    elif area.status == 'GAMING':
                                        self.hub.status = 'GAMING'
                                        break
                                    elif area.status == 'RP':
                                        self.hub.status = 'RP'
                                        break
                    self.server.area_manager.send_arup_status()
                if self.is_restricted:
                    self.conn_arup_status()
                else:
                    self.hub.sub_arup_status()

            elif self.is_hub:
                self.sub_arup_status()
                self.server.area_manager.send_arup_status()
            else:
                self.server.area_manager.send_arup_status()

        def hub_status(self, value):
            """
			Set the status of all areas in a hub.
			:param value: status code
			"""
            allowed_values = ('idle', 'rp', 'casing', 'looking-for-players',
                              'lfp', 'recess', 'gaming')
            if value.lower() not in allowed_values:
                raise AreaError(
                    f'Invalid status. Possible values: {", ".join(allowed_values)}'
                )
            if value.lower() == 'lfp':
                value = 'looking-for-players'
            self.status = value.upper()
            for area in self.subareas:
                area.status = value.upper()
                if area.is_restricted:
                    self.conn_arup_status()
            self.sub_arup_status()
            self.server.area_manager.send_arup_status()

        def change_doc(self, doc='No document.'):
            """
			Set the doc link.
			:param doc: doc link (Default value = 'No document.')
			"""
            self.doc = doc

        def add_to_judgelog(self, client, msg):
            """
			Append an event to the judge log (max 10 items).
			:param client: event origin
			:param msg: event message
			"""
            if len(self.judgelog) >= 10:
                self.judgelog = self.judgelog[1:]
            self.judgelog.append(f'{client.char_name} ({client.ip}) {msg}.')

        def add_music_playing(self, client, name):
            """
			Set info about the current track playing.
			:param client: player
			:param name: track name
			"""
            self.current_music_player = client.char_name
            self.current_music_player_ipid = client.ipid
            self.current_music = name
            for c in self.clients:
                c.current_music = name

        def add_music_playing_shownamed(self, client, showname, name):
            """
			Set info about the current track playing.
			:param client: player
			:param showname: showname of player
			:param name: track name
			"""
            self.current_music_player = f'{showname} ({client.char_name})'
            self.current_music_player_ipid = client.ipid
            self.current_music = name
            for c in self.clients:
                c.current_music = name

        def get_evidence_list(self, client):
            """
			Get the evidence list of the area.
			:param client: requester
			"""
            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
			Broadcast an updated evidence list.
			LE#<name>&<desc>&<img>#<name>
			"""
            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))

        def get_cms(self):
            """
			Get a list of CMs.
			:return: message
			"""
            msg = ''
            for i in self.owners:
                if not i.ghost:
                    msg += f'[{str(i.id)}] {i.char_name}, '
            if len(msg) > 2:
                msg = msg[:-2]
            return msg

        def get_mods(self):
            mods = set()
            for client in self.clients:
                if client.is_mod:
                    mods.add(client)
            return mods

        def get_sub(self, name):
            for area in self.subareas:
                if area.name == name:
                    return area
            raise AreaError('Area not found.')

        def get_music(self, client):
            song_list = []
            music_list = self.server.music_list
            for item in music_list:
                song_list.append(item['category'])
                for song in item['songs']:
                    song_list.append(song['name'])
            if len(self.cmusic_list) != 0:
                for item in self.cmusic_list:
                    song_list.append(item['category'])
                    if len(item['songs']) != 0:
                        for song in item['songs']:
                            song_list.append(song['name'])
            return song_list

        def conn_arup_players(self):
            players_list = [0]
            lobby = self.server.area_manager.default_area()
            players_list.append(len(lobby.clients))
            if self.hub.hidden:
                players_list.append(-1)
            else:
                players_list.append(len(self.hub.clients))
            if self.hidden:
                players_list.append(-1)
            else:
                players_list.append(len(self.clients))
            for link in self.connections:
                if link != lobby and link != self.hub:
                    if link.hidden:
                        players_list.append(-1)
                    else:
                        players_list.append(len(link.clients))
            self.server.send_conn_arup(players_list, self)

        def conn_arup_status(self):
            """Broadcast ARUP packet containing area statuses."""
            status_list = [1]
            lobby = self.server.area_manager.default_area()
            status_list.append(lobby.status)
            status_list.append(self.hub.status)
            status_list.append(self.status)
            for link in self.connections:
                if link != lobby and link != self.hub:
                    status_list.append(link.status)
            self.server.send_conn_arup(status_list, self)

        def conn_arup_cms(self):
            """Broadcast ARUP packet containing area CMs."""
            cms_list = [2]
            lobby = self.server.area_manager.default_area()
            if len(lobby.owners) == 0:
                cms_list.append('FREE')
            else:
                cms_list.append(lobby.get_cms())
            if len(self.hub.owners) == 0:
                cms_list.append('FREE')
            else:
                cms_list.append(self.hub.get_cms())
            if len(self.owners) == 0:
                cms_list.append('FREE')
            else:
                cms_list.append(self.get_cms())
            for link in self.connections:
                if link != lobby and link != self.hub:
                    cm = 'FREE'
                    if len(link.owners) > 0:
                        cm = link.get_cms()
                    cms_list.append(cm)
            self.server.send_conn_arup(cms_list, self)

        def conn_arup_lock(self):
            """Broadcast ARUP packet containing the lock status of each area."""
            lock_list = [3]
            lobby = self.server.area_manager.default_area()
            lock_list.append(lobby.is_locked.name)
            lock_list.append(self.hub.is_locked.name)
            lock_list.append(self.is_locked.name)
            for link in self.connections:
                if link != lobby and link != self.hub:
                    lock_list.append(link.is_locked.name)
            self.server.send_hub_arup(lock_list, self)

        def sub_arup_players(self, client=None):
            """Broadcast ARUP packet containing player counts."""
            players_list = [0]
            lobby = self.server.area_manager.default_area()
            players_list.append(len(lobby.clients))
            players_list.append(len(self.clients))
            for area in self.subareas:
                if area.hidden == True:
                    players_list.append(-1)
                else:
                    index = 0
                    for c in area.clients:
                        if not c.ghost and not c.hidden:
                            index += 1
                    players_list.append(index)
            if client != None:
                client.send_self_arup(players_list)
            else:
                self.server.send_hub_arup(players_list, self)

        def sub_arup_status(self, client=None):
            """Broadcast ARUP packet containing area statuses."""
            status_list = [1]
            lobby = self.server.area_manager.default_area()
            status_list.append(lobby.status)
            status_list.append(self.status)
            for area in self.subareas:
                status_list.append(area.status)
            if client != None:
                client.send_self_arup(status_list)
            else:
                self.server.send_hub_arup(status_list, self)

        def sub_arup_cms(self, client=None):
            """Broadcast ARUP packet containing area CMs."""
            cms_list = [2]
            lobby = self.server.area_manager.default_area()
            if len(lobby.owners) == 0:
                cms_list.append('FREE')
            else:
                cms_list.append(lobby.get_cms())
            if len(self.owners) == 0:
                cms_list.append('FREE')
            else:
                cms_list.append(self.get_cms())
            for area in self.subareas:
                cm = 'FREE'
                if len(area.owners) > 0:
                    cm = area.get_cms()
                cms_list.append(cm)
            if client != None:
                client.send_self_arup(cms_list)
            else:
                self.server.send_hub_arup(cms_list, self)

        def sub_arup_lock(self, client=None):
            """Broadcast ARUP packet containing the lock status of each area."""
            lock_list = [3]
            lobby = self.server.area_manager.default_area()
            lock_list.append(lobby.is_locked.name)
            lock_list.append(self.is_locked.name)
            for area in self.subareas:
                lock_list.append(area.is_locked.name)
            if client != None:
                client.send_self_arup(lock_list)
            else:
                self.server.send_hub_arup(lock_list, self)

        def broadcast_hub(self, client, msg):
            char_name = client.char_name
            ooc_name = '{}[{}][{}]'.format('<dollar>H',
                                           client.area.abbreviation, char_name)
            if client.area.sub:
                if client in client.area.hub.owners:
                    ooc_name += '[CM]'
                self.server.send_all_cmd_pred(
                    'CT',
                    ooc_name,
                    msg,
                    pred=lambda x: x.area in client.area.hub.subareas)
                self.server.send_all_cmd_pred(
                    'CT',
                    ooc_name,
                    msg,
                    pred=lambda x: x.area is client.area.hub)
            else:
                if client in client.area.owners:
                    ooc_name += '[CM]'
                self.server.send_all_cmd_pred(
                    'CT',
                    ooc_name,
                    msg,
                    pred=lambda x: x.area in client.area.subareas)
                self.server.send_all_cmd_pred(
                    'CT', ooc_name, msg, pred=lambda x: x.area is client.area)

        class JukeboxVote:
            """Represents a single vote cast for the jukebox."""
            def __init__(self, client, name, length, showname):
                self.client = client
                self.name = name
                self.length = length
                self.chance = 1
                self.showname = showname
示例#7
0
    class Area:
        def __init__(self, area_id, server, parameters):
            self.clients = set()
            self.invite_list = {}
            self.id = area_id
            self.server = server
            self.music_looper = None
            self.next_message_time = 0
            self.hp_def = 10
            self.hp_pro = 10
            self.doc = 'No document.'
            self.status = 'IDLE'
            self.judgelog = []
            self.current_music = ''
            self.current_music_player = ''
            self.evi_list = EvidenceList()
            self.is_recording = False
            self.recorded_messages = []
            self.owned = False
            self.ic_lock = False
            self.is_locked = False
            self.is_gmlocked = False
            self.is_modlocked = False
            self.bleeds_to = set()
            self.lights = True

            self.name = parameters['area']
            self.background = parameters['background']
            self.bg_lock = parameters['bglock']
            self.evidence_mod = parameters['evidence_mod']
            self.locking_allowed = parameters['locking_allowed']
            self.iniswap_allowed = parameters['iniswap_allowed']
            self.rp_getarea_allowed = parameters['rp_getarea_allowed']
            self.rp_getareas_allowed = parameters['rp_getareas_allowed']
            self.rollp_allowed = parameters['rollp_allowed']
            self.reachable_areas = parameters['reachable_areas']
            self.change_reachability_allowed = parameters[
                'change_reachability_allowed']
            self.default_change_reachability_allowed = parameters[
                'change_reachability_allowed']
            self.gm_iclock_allowed = parameters['gm_iclock_allowed']
            self.afk_delay = parameters['afk_delay']
            self.afk_sendto = parameters['afk_sendto']
            self.lobby_area = parameters['lobby_area']
            self.private_area = parameters['private_area']
            self.scream_range = parameters['scream_range']
            self.restricted_chars = parameters['restricted_chars']
            self.default_description = parameters['default_description']
            self.has_lights = parameters['has_lights']

            self.description = self.default_description  # Store the current description separately from the default description
            self.background_backup = self.background  # Used for restoring temporary background changes
            # Fix comma-separated entries
            self.reachable_areas = fix_and_setify(self.reachable_areas)
            self.scream_range = fix_and_setify(self.scream_range)
            self.restricted_chars = fix_and_setify(self.restricted_chars)

            self.default_reachable_areas = self.reachable_areas.copy()
            self.staffset_reachable_areas = self.reachable_areas.copy()

            if '<ALL>' not in self.reachable_areas:
                self.reachable_areas.add(self.name)  #Safety feature, yay sets

            # Make sure only characters that exist are part of the restricted char set
            try:
                for char_name in self.restricted_chars:
                    self.server.char_list.index(char_name)
            except ValueError:
                info = (
                    'Area {} has an unrecognized character {} as a restricted character. '
                    'Please make sure this character exists and try again.'.
                    format(self.name, char_name))
                raise AreaError(info)

        def new_client(self, client):
            self.clients.add(client)

        def remove_client(self, client):
            self.clients.remove(client)
            if len(self.clients) == 0:
                self.unlock()

        def unlock(self):
            self.is_locked = False
            if not self.is_gmlocked and not self.is_modlocked:
                self.invite_list = {}

        def gmunlock(self):
            self.is_gmlocked = False
            self.is_locked = False
            if not self.is_modlocked:
                self.invite_list = {}

        def modunlock(self):
            self.is_modlocked = False
            self.is_gmlocked = False
            self.is_locked = False
            self.invite_list = {}

        def get_chars_unusable(self, allow_restricted=False):
            if allow_restricted:
                return set(
                    [x.char_id for x in self.clients if x.char_id is not None])
            return set([
                x.char_id for x in self.clients if x.char_id is not None
            ]).union(
                set([
                    self.server.char_list.index(char_name)
                    for char_name in self.restricted_chars
                ]))

        def is_char_available(self, char_id, allow_restricted=False):
            return (char_id == -1) or (char_id not in self.get_chars_unusable(
                allow_restricted=allow_restricted))

        def get_rand_avail_char_id(self, allow_restricted=False):
            avail_set = set(range(len(
                self.server.char_list))) - self.get_chars_unusable(
                    allow_restricted=allow_restricted)
            if len(avail_set) == 0:
                raise AreaError('No available characters.')
            return random.choice(tuple(avail_set))

        def send_command(self, cmd, *args):
            for c in self.clients:
                c.send_command(cmd, *args)

        def send_host_message(self, msg):
            self.send_command('CT', self.server.config['hostname'], msg)

        def set_next_msg_delay(self, msg_length):
            delay = min(3000, 100 + 60 * msg_length)
            self.next_message_time = round(time.time() * 1000.0 + delay)

        def is_iniswap(self, client, anim1, anim2, char):
            if self.iniswap_allowed:
                return False
            if '..' in anim1 or '..' in anim2:
                return True
            for char_link in self.server.allowed_iniswaps:
                if client.get_char_name() in char_link and char in char_link:
                    return False
            return True

        def play_music(self, name, cid, length=-1):
            self.send_command('MC', name, cid)
            if self.music_looper:
                self.music_looper.cancel()
            if length > 0:
                self.music_looper = asyncio.get_event_loop().call_later(
                    length, lambda: self.play_music(name, -1, length))

        def can_send_message(self):
            return (time.time() * 1000.0 - self.next_message_time) > 0

        def change_hp(self, side, val):
            if not 0 <= val <= 10:
                raise AreaError('Invalid penalty value.')
            if not 1 <= side <= 2:
                raise AreaError('Invalid penalty side.')
            if side == 1:
                self.hp_def = val
            elif side == 2:
                self.hp_pro = val
            self.send_command('HP', side, val)

        def change_background(self, bg):
            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError('Invalid background name.')
            self.background = bg
            self.send_command('BN', self.background)

        def change_background_mod(self, bg):
            self.background = bg
            self.send_command('BN', self.background)

        def change_lights(self, new_lights, initiator=None):
            status = {True: 'on', False: 'off'}

            if new_lights:
                if self.background == self.server.config[
                        'blackout_background']:
                    intended_background = self.background_backup
                else:
                    intended_background = self.background
            else:
                if self.background != self.server.config['blackout_background']:
                    self.background_backup = self.background
                intended_background = self.server.config['blackout_background']

            try:
                self.change_background(intended_background)
            except AreaError:
                raise AreaError(
                    'Unable to turn lights {}: Background {} not found'.format(
                        status[new_lights], intended_background))

            self.lights = new_lights

            if initiator:  # If a player initiated the change light sequence, send targeted messages
                initiator.send_host_message('You turned the lights {}.'.format(
                    status[new_lights]))
                self.server.send_all_cmd_pred(
                    'CT',
                    '{}'.format(self.server.config['hostname']),
                    'The lights were turned {}.'.format(status[new_lights]),
                    pred=lambda c: not c.is_staff(
                    ) and c.area == self and c != initiator)
                self.server.send_all_cmd_pred(
                    'CT',
                    '{}'.format(self.server.config['hostname']),
                    '{} turned the lights {}.'.format(
                        initiator.get_char_name(), status[new_lights]),
                    pred=lambda c: c.is_staff(
                    ) and c.area == self and c != initiator)
            else:  # Otherwise, send generic message
                self.send_host_message('The lights were turned {}.'.format(
                    status[new_lights]))

            # Reveal people bleeding and not sneaking if lights were turned on
            if self.lights:
                for c in self.clients:
                    bleeding_visible = [
                        x for x in self.clients
                        if x.is_visible and x.is_bleeding and x != c
                    ]
                    info = ''

                    if len(bleeding_visible) == 1:
                        info = 'You now see {} is bleeding.'.format(
                            bleeding_visible[0].get_char_name())
                    elif len(bleeding_visible) > 1:
                        info = 'You now see {}'.format(
                            bleeding_visible[0].get_char_name())
                        for i in range(1, len(bleeding_visible) - 1):
                            info += ', {}'.format(
                                bleeding_visible[i].get_char_name())
                        info += ' and {} are bleeding.'.format(
                            bleeding_visible[-1].get_char_name())

                    if info:
                        c.send_host_message(info)

        def change_status(self, value):
            allowed_values = ('idle', 'building-open', 'building-full',
                              'casing-open', 'casing-full', 'recess')
            if value.lower() not in allowed_values:
                raise AreaError('Invalid status. Possible values: {}'.format(
                    ', '.join(allowed_values)))
            self.status = value.upper()

        def change_doc(self, doc='No document.'):
            self.doc = doc

        def add_to_judgelog(self, client, msg):
            if len(self.judgelog) >= 10:
                self.judgelog = self.judgelog[1:]
            self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(),
                                                      client.get_ip(), msg))

        def add_music_playing(self, client, name):
            self.current_music_player = client.get_char_name()
            self.current_music = name

        def get_evidence_list(self, client):
            client.evi_list, evi_list = self.evi_list.create_evi_list(client)
            return evi_list

        def broadcast_evidence_list(self):
            """
                LE#<name>&<desc>&<img>#<name>

            """
            for client in self.clients:
                client.send_command('LE', *self.get_evidence_list(client))
示例#8
0
class Area:
    """Represents a single instance of an area."""
    def __init__(self, area_manager, name):
        self.clients = set()
        self.invite_list = set()
        self.area_manager = area_manager
        self._name = name

        # Initialize prefs
        self.background = 'default'
        self.pos_lock = []
        self.bg_lock = False
        self.evidence_mod = 'FFA'
        self.can_cm = False
        self.locking_allowed = False
        self.iniswap_allowed = True
        self.showname_changes_allowed = True
        self.shouts_allowed = True
        self.jukebox = False
        self.abbreviation = self.abbreviate()
        self.non_int_pres_only = False
        self.locked = False
        self.muted = False
        self.blankposting_allowed = True
        self.hp_def = 10
        self.hp_pro = 10
        self.doc = 'No document.'
        self.status = 'IDLE'
        self.move_delay = 0
        self.hide_clients = False
        self.max_players = -1
        self.desc = ''
        self.music_ref = ''
        self.client_music = True
        self.replace_music = False
        self.ambience = ''
        self.can_dj = True
        self.hidden = False
        self.can_whisper = True
        self.can_wtce = True
        self.music_autoplay = False
        self.can_change_status = True
        self.use_backgrounds_yaml = False
        self.can_spectate = True
        self.can_getarea = True
        self.can_cross_swords = False
        self.can_scrum_debate = False
        self.can_panic_talk_action = False
        # /prefs end

        # DR minigames
        # in seconds, 300s = 5m
        self.cross_swords_timer = 300
        # in seconds, 300s = 5m. How much time is added on top of cross swords.
        self.scrum_debate_added_time = 300
        # in seconds, 300s = 5m
        self.panic_talk_action_timer = 300
        # Cooldown in seconds, 300s = 5m
        self.minigame_cooldown = 300
        # Who's debating who
        self.red_team = set()
        self.blue_team = set()
        # Minigame name
        self.minigame = ''
        # Minigame schedule
        self.minigame_schedule = None
        # /end

        self.old_muted = False
        self.old_invite_list = set()

        # original states for resetting the area after all CMs leave in a single area CM hub
        self.o_name = self._name
        self.o_abbreviation = self.abbreviation
        self.o_doc = self.doc
        self.o_desc = self.desc
        self.o_background = self.background

        self.music_looper = None
        self.next_message_time = 0
        self.judgelog = []
        self.music = ''
        self.music_player = ''
        self.music_player_ipid = -1
        self.music_looping = 0
        self.music_effects = 0
        self.evi_list = EvidenceList()
        self.testimony = []
        self.testimony_title = ''
        self.testimony_index = -1
        self.recording = False
        self.last_ic_message = None
        self.cards = dict()
        self.votes = dict()
        self.password = ''

        self.jukebox_votes = []
        self.jukebox_prev_char_id = -1

        self.music_list = []

        self._owners = set()
        self.afkers = []

        # Dictionary of dictionaries with further info, examine def link for more info
        self.links = {}

    @property
    def name(self):
        """Area's name string. Abbreviation is also updated according to this."""
        return self._name

    @name.setter
    def name(self, value):
        self._name = value
        self.abbreviation = self.abbreviate()

    @property
    def id(self):
        """Get area's index in the AreaManager's 'areas' list."""
        return self.area_manager.areas.index(self)

    @property
    def server(self):
        """Area's server. Accesses AreaManager's 'server' property"""
        return self.area_manager.server

    @property
    def owners(self):
        """Area's owners. Also appends Game Masters (Hub Managers)."""
        return self.area_manager.owners | self._owners

    def abbreviate(self):
        """Abbreviate our name."""
        if self.name.lower().startswith("courtroom"):
            return "CR" + self.name.split()[-1]
        elif self.name.lower().startswith("area"):
            return "A" + self.name.split()[-1]
        elif len(self.name.split()) > 1:
            return "".join(item[0].upper() for item in self.name.split())
        elif len(self.name) > 3:
            return self.name[:3].upper()
        else:
            return self.name.upper()

    def load(self, area):
        self._name = area['area']
        self.o_name = self._name
        self.o_abbreviation = self.abbreviation
        _pos_lock = ''
        # Legacy KFO support.
        # We gotta fix the sins of our forefathers
        if 'poslock' in area:
            _pos_lock = area['poslock'].split(' ')
        if 'bglock' in area:
            self.bg_lock = area['bglock']
        if 'accessible' in area:
            self.links.clear()
            for link in [s for s in str(area['accessible']).split(' ')]:
                self.link(link)

        if 'is_locked' in area:
            self.locked = False
            self.muted = False
            if area['is_locked'] == 'SPECTATABLE':
                self.muted = True
            elif area['is_locked'] == 'LOCKED':
                self.locked = True

        if 'background' in area:
            self.background = area['background']
            self.o_background = self.background
        if 'bg_lock' in area:
            self.bg_lock = area['bg_lock']
        if 'pos_lock' in area:
            _pos_lock = area['pos_lock'].split(' ')

        if len(_pos_lock) > 0:
            self.pos_lock.clear()
            for pos in _pos_lock:
                pos = pos.lower()
                if pos != "none" and not (pos in self.pos_lock):
                    self.pos_lock.append(pos.lower())

        if 'evidence_mod' in area:
            self.evidence_mod = area['evidence_mod']
        if 'can_cm' in area:
            self.can_cm = area['can_cm']
        if 'locking_allowed' in area:
            self.locking_allowed = area['locking_allowed']
        if 'iniswap_allowed' in area:
            self.iniswap_allowed = area['iniswap_allowed']
        if 'showname_changes_allowed' in area:
            self.showname_changes_allowed = area['showname_changes_allowed']
        if 'shouts_allowed' in area:
            self.shouts_allowed = area['shouts_allowed']
        if 'jukebox' in area:
            self.jukebox = area['jukebox']
        if 'abbreviation' in area:
            self.abbreviation = area['abbreviation']
        else:
            self.abbreviation = self.abbreviate()
        if 'non_int_pres_only' in area:
            self.non_int_pres_only = area['non_int_pres_only']
        if 'locked' in area:
            self.locked = area['locked']
        if 'muted' in area:
            self.muted = area['muted']
        if 'blankposting_allowed' in area:
            self.blankposting_allowed = area['blankposting_allowed']
        if 'hp_def' in area:
            self.hp_def = area['hp_def']
        if 'hp_pro' in area:
            self.hp_pro = area['hp_pro']
        if 'doc' in area:
            self.doc = area['doc']
            self.o_doc = self.doc
        if 'status' in area:
            self.status = area['status']
        if 'move_delay' in area:
            self.move_delay = area['move_delay']
        if 'hide_clients' in area:
            self.hide_clients = area['hide_clients']
        if 'music_autoplay' in area:
            self.music_autoplay = area['music_autoplay']
            if self.music_autoplay and 'music' in area:
                self.music = area['music']
                self.music_effects = area['music_effects']
                self.music_looping = area['music_looping']
        if 'max_players' in area:
            self.max_players = area['max_players']
        if 'desc' in area:
            self.desc = area['desc']
            self.o_desc = self.desc
        if 'music_ref' in area:
            self.clear_music()
            self.music_ref = area['music_ref']
        if self.music_ref != '':
            self.load_music(f'storage/musiclists/{self.music_ref}.yaml')

        if 'client_music' in area:
            self.client_music = area['client_music']
        if 'replace_music' in area:
            self.replace_music = area['replace_music']
        if 'ambience' in area:
            self.ambience = area['ambience']
        if 'can_dj' in area:
            self.can_dj = area['can_dj']
        if 'hidden' in area:
            self.hidden = area['hidden']
        if 'can_whisper' in area:
            self.can_whisper = area['can_whisper']
        if 'can_wtce' in area:
            self.can_wtce = area['can_wtce']
        if 'can_change_status' in area:
            self.can_change_status = area['can_change_status']
        if 'use_backgrounds_yaml' in area:
            self.use_backgrounds_yaml = area['use_backgrounds_yaml']
        if 'can_spectate' in area:
            self.can_spectate = area['can_spectate']
        if 'can_getarea' in area:
            self.can_getarea = area['can_getarea']
        if 'password' in area:
            self.password = area['password']

        if 'evidence' in area and len(area['evidence']) > 0:
            self.evi_list.evidences.clear()
            self.evi_list.import_evidence(area['evidence'])
            self.broadcast_evidence_list()

        if 'links' in area and len(area['links']) > 0:
            self.links.clear()
            for key, value in area['links'].items():
                locked, hidden, target_pos, can_peek, evidence, password = False, False, '', True, [], ''
                if 'locked' in value:
                    locked = value['locked']
                if 'hidden' in value:
                    hidden = value['hidden']
                if 'target_pos' in value:
                    target_pos = value['target_pos']
                if 'can_peek' in value:
                    can_peek = value['can_peek']
                if 'evidence' in value:
                    evidence = value['evidence']
                if 'password' in value:
                    password = value['password']
                self.link(key, locked, hidden, target_pos, can_peek, evidence,
                          password)

        # Update the clients in that area
        self.change_background(self.background)
        self.change_hp(1, self.hp_def)
        self.change_hp(2, self.hp_pro)
        if self.ambience:
            self.set_ambience(self.ambience)
        if self.music_autoplay:
            for client in self.clients:
                client.send_command('MC', self.music, -1, '',
                                    self.music_looping, 0, self.music_effects)

    def save(self):
        area = OrderedDict()
        area['area'] = self.name
        area['background'] = self.background
        area['pos_lock'] = 'none'
        if len(self.pos_lock) > 0:
            area['pos_lock'] = ' '.join(map(str, self.pos_lock))
        area['bg_lock'] = self.bg_lock
        area['evidence_mod'] = self.evidence_mod
        area['can_cm'] = self.can_cm
        area['locking_allowed'] = self.locking_allowed
        area['iniswap_allowed'] = self.iniswap_allowed
        area['showname_changes_allowed'] = self.showname_changes_allowed
        area['shouts_allowed'] = self.shouts_allowed
        area['jukebox'] = self.jukebox
        area['abbreviation'] = self.abbreviation
        area['non_int_pres_only'] = self.non_int_pres_only
        area['locked'] = self.locked
        area['muted'] = self.muted
        area['blankposting_allowed'] = self.blankposting_allowed
        area['hp_def'] = self.hp_def
        area['hp_pro'] = self.hp_pro
        area['doc'] = self.doc
        area['status'] = self.status
        area['move_delay'] = self.move_delay
        area['hide_clients'] = self.hide_clients
        area['music_autoplay'] = self.music_autoplay
        area['max_players'] = self.max_players
        area['desc'] = self.desc
        if self.music_ref != '':
            area['music_ref'] = self.music_ref
            area['replace_music'] = self.replace_music
        area['client_music'] = self.client_music
        if self.music_autoplay:
            area['music'] = self.music
            area['music_effects'] = self.music_effects
            area['music_looping'] = self.music_looping
        area['ambience'] = self.ambience
        area['can_dj'] = self.can_dj
        area['hidden'] = self.hidden
        area['can_whisper'] = self.can_whisper
        area['can_wtce'] = self.can_wtce
        area['can_change_status'] = self.can_change_status
        area['use_backgrounds_yaml'] = self.use_backgrounds_yaml
        area['can_spectate'] = self.can_spectate
        area['can_getarea'] = self.can_getarea
        area['password'] = self.password
        if len(self.evi_list.evidences) > 0:
            area['evidence'] = [e.to_dict() for e in self.evi_list.evidences]
        if len(self.links) > 0:
            area['links'] = self.links
        return area

    def new_client(self, client):
        """Add a client to the area."""
        self.clients.add(client)
        database.log_area('area.join', client, self)

        if self.music_autoplay:
            client.send_command('MC', self.music, -1, '', self.music_looping,
                                0, self.music_effects)

        # Play the ambience
        client.send_command(
            'MC', self.ambience, -1, "", 1, 1,
            int(MusicEffect.FADE_OUT | MusicEffect.FADE_IN
                | MusicEffect.SYNC_POS))

    def remove_client(self, client):
        """Remove a disconnected client from the area."""
        if client.hidden_in != None:
            client.hide(False, hidden=True)
        if self.area_manager.single_cm:
            # Remove their owner status due to single_cm pref. remove_owner will unlock the area if they were the last CM.
            if client in self.owners:
                self.remove_owner(client)
                client.send_ooc(
                    'You can only be a CM of a single area in this hub.')
        if self.locking_allowed:
            # Since anyone can lock/unlock, unlock if we were the last client in this area and it was locked.
            if len(self.clients) - 1 <= 0:
                if self.locked:
                    self.unlock()
        self.clients.remove(client)
        if client in self.afkers:
            self.afkers.remove(client)
            self.server.client_manager.toggle_afk(client)
        if self.jukebox:
            self.remove_jukebox_vote(client, True)
        if len(self.clients) == 0:
            self.change_status('IDLE')
        database.log_area('area.leave', client, self)
        if not client.hidden:
            self.area_manager.send_arup_players()

        # Update everyone's available characters list
        # Commented out due to potentially causing clientside lag...
        # self.send_command('CharsCheck',
        #                     *client.get_available_char_list())

    def unlock(self):
        """Mark the area as unlocked."""
        self.locked = False
        self.area_manager.send_arup_lock()

    def lock(self):
        """Mark the area as locked."""
        self.locked = True
        self.area_manager.send_arup_lock()

    def mute(self):
        """Mute the area."""
        self.muted = True
        self.invite_list.clear()
        self.area_manager.send_arup_lock()

    def unmute(self):
        """Unmute the area."""
        self.muted = False
        self.invite_list.clear()
        self.area_manager.send_arup_lock()

    def link(self,
             target,
             locked=False,
             hidden=False,
             target_pos='',
             can_peek=True,
             evidence=[],
             password=''):
        """
        Sets up a one-way connection between this area and targeted area.
        Returns the link dictionary.
        :param target: the targeted Area ID to connect
        :param locked: is the link unusable?
        :param hidden: is the link invisible?
        :param target_pos: which position should we end up in when we come through
        :param can_peek: can you peek through this path?
        :param evidence: a list of evidence from which this link will be accessible when you hide in it

        """
        link = {
            "locked": locked,
            "hidden": hidden,
            "target_pos": target_pos,
            "can_peek": can_peek,
            "evidence": evidence,
            "password": password,
        }
        self.links[str(target)] = link
        return link

    def unlink(self, target):
        try:
            del self.links[str(target)]
        except KeyError:
            raise AreaError(
                f'Link {target} does not exist in Area {self.name}!')

    def is_char_available(self, char_id):
        """
        Check if a character is available for use.
        :param char_id: character ID
        """
        return char_id not in [x.char_id for x in self.clients]

    def get_rand_avail_char_id(self):
        """Get a random available character ID."""
        avail_set = set(range(len(
            self.server.char_list))) - {x.char_id
                                        for x in self.clients}
        if len(avail_set) == 0:
            raise AreaError('No available characters.')
        return random.choice(tuple(avail_set))

    def send_command(self, cmd, *args):
        """
        Broadcast an AO-compatible command to all clients in the area.
        """
        for c in self.clients:
            c.send_command(cmd, *args)

    def send_owner_command(self, cmd, *args):
        """
        Send an AO-compatible command to all owners of the area
        that are not currently in the area.
        """
        for c in self.owners:
            if c in self.clients:
                continue
            if c.remote_listen == 3 or \
                    (cmd == 'CT' and c.remote_listen == 2) or \
                    (cmd == 'MS' and c.remote_listen == 1):
                c.send_command(cmd, *args)

    def broadcast_ooc(self, msg):
        """
        Broadcast an OOC message to all clients in the area.
        :param msg: message
        """
        self.send_command('CT', self.server.config['hostname'], msg, '1')
        self.send_owner_command(
            'CT', f'[{self.id}]' + self.server.config['hostname'], msg, '1')

    # Please forgive my sin
    def send_ooc(self, name, msg):
        self.send_command('CT', name, msg, '1')

    def send_ic(self, client, *args, targets=None):
        """
        Send an IC message from a client to all applicable clients in the area.
        :param client: speaker
        :param *args: arguments
        """
        if client in self.afkers:
            client.server.client_manager.toggle_afk(client)
        if client and args[4].startswith('**') and len(self.testimony) > 0:
            idx = self.testimony_index
            if idx == -1:
                idx = 0
            try:
                lst = list(self.testimony[idx])
                lst[4] = "}}}" + args[4][2:]
                self.testimony[idx] = tuple(lst)
                self.broadcast_ooc(
                    f'{client.showname} has amended Statement {idx+1}.')
                if not self.recording:
                    self.testimony_send(idx)
            except IndexError:
                client.send_ooc(
                    f'Something went wrong, couldn\'t amend Statement {idx+1}!'
                )
            return
        adding = args[4].strip() != '' and self.recording and client != None
        if client and args[4].startswith('++') and len(self.testimony) > 0:
            if len(self.testimony) >= 30:
                client.send_ooc(
                    'Maximum testimony statement amount reached! (30)')
                return
            adding = True
        else:
            if targets == None:
                targets = self.clients
            for c in targets:
                # Blinded clients don't receive IC messages
                if c.blinded:
                    continue
                # pos doesn't match listen_pos, we're not listening so make this an OOC message instead
                if c.listen_pos != None:
                    if type(c.listen_pos) is list and not (args[5] in c.listen_pos) or \
                        c.listen_pos == 'self' and args[5] != c.pos:
                        name = ''
                        if args[8] != -1:
                            name = self.server.char_list[args[8]]
                        if args[15] != '':
                            name = args[15]
                        # Send the mesage as OOC.
                        # Woulda been nice if there was a packet to send messages to IC log
                        # without displaying it in the viewport.
                        c.send_command('CT', f'[pos \'{args[5]}\'] {name}',
                                       args[4])
                        continue
                c.send_command('MS', *args)

            # args[4] = msg
            # args[15] = showname
            name = ''
            if args[8] != -1:
                name = self.server.char_list[args[8]]
            if args[15] != '':
                name = args[15]

            delay = 200 + self.parse_msg_delay(args[4])
            self.next_message_time = round(time.time() * 1000.0 + delay)

            # Objection used
            if int(args[10]) == 2:
                msg = args[4].lower()
                target = None
                is_pta = False
                if self.last_ic_message != None:
                    # Get char_name from character ID
                    target = self.server.char_list[int(
                        self.last_ic_message[8])]
                # contains word "pta" in message
                if ' pta' in f' {msg} ':
                    # formatting for `PTA @Jack` or `@Jack PTA`
                    is_pta = True

                # message contains an "at" sign aka we're referring to someone specific
                if '@' in msg:
                    # formatting for `PTA@Jack`
                    if msg.startswith('pta'):
                        is_pta = True
                    target = msg[msg.find('@') + 1:]

                try:
                    for t in self.clients:
                        # I apologize for this monstrosity.
                        if t.showname.lower().startswith(
                                target) or t.showname.lower().startswith(
                                    target.split()[0]) or (
                                        t.name != '' and
                                        (t.name.lower().startswith(target)
                                         or t.name.lower().startswith(
                                             target.split()[0]))):
                            self.start_debate(client, t, is_pta)
                            break
                except Exception as ex:
                    client.send_ooc(ex)
                    return

            if client:
                if args[4].strip(
                ) != '' or self.last_ic_message == None or args[
                        8] != self.last_ic_message[8] or self.last_ic_message[
                            4].strip() != '':
                    database.log_area('chat.ic',
                                      client,
                                      client.area,
                                      message=args[4])
                if self.recording:
                    # See if the testimony is supposed to end here.
                    scrunched = ''.join(e for e in args[4] if e.isalnum())
                    if len(scrunched) > 0 and scrunched.lower() == 'end':
                        self.recording = False
                        self.broadcast_ooc(
                            f'[{client.id}] {client.showname} has ended the testimony.'
                        )
                        return
            self.last_ic_message = args

        if adding:
            if len(self.testimony) >= 30:
                client.send_ooc(
                    'Maximum testimony statement amount reached! (30)')
                return
            lst = list(args)
            if lst[4].startswith('++'):
                lst[4] = lst[4][2:]
            # Remove speed modifying chars and start the statement instantly
            lst[4] = "}}}" + lst[4].replace('{', '').replace('}', '')
            # Non-int pre automatically enabled
            lst[18] = 1
            # Set emote_mod to conform to nonint_pre
            if lst[7] == 1 or lst[7] == 2:
                lst[7] = 0
            elif lst[7] == 6:
                lst[7] = 5
            # Make it green
            lst[14] = 1
            rec = tuple(lst)
            idx = self.testimony_index
            if idx == -1:
                # Add one statement at the very end.
                self.testimony.append(rec)
                idx = self.testimony.index(rec)
            else:
                # Add one statement ahead of the one we're currently on.
                idx += 1
                self.testimony.insert(idx, rec)
            self.broadcast_ooc(f'Statement {idx+1} added.')
            if not self.recording:
                self.testimony_send(idx)

    def testimony_send(self, idx):
        """Send the testimony statement at index"""
        try:
            statement = self.testimony[idx]
            self.testimony_index = idx
            targets = self.clients
            for c in targets:
                # Blinded clients don't receive IC messages
                if c.blinded:
                    continue
                # Ignore those losers with listenpos for testimony
                c.send_command('MS', *statement)
        except (ValueError, IndexError):
            raise AreaError('Invalid testimony reference!')

    def parse_msg_delay(self, msg):
        """ Parses the correct delay for the message supporting escaped characters and }}} {{{ speed-ups/slowdowns.
        :param msg: the string
        :return: delay integer in ms
        """
        #Fastest - Default - Slowest. These are default values in ms for KFO Client.
        message_display_speed = [0, 10, 25, 40, 50, 70, 90]

        #Starts in the middle of the messageDisplaySpeed list
        current_display_speed = 3

        #The 'meh' part of this is we can't exactly calculate accurately if color chars are used (as they could change clientside).
        formatting_chars = "@$`|_~%\\}{"

        calculated_delay = 0

        escaped = False

        for symbol in msg:
            if symbol in formatting_chars and not escaped:
                if symbol == "\\":
                    escaped = True
                elif symbol == "{":  #slow down
                    current_display_speed = min(
                        len(message_display_speed) - 1,
                        current_display_speed + 1)
                elif symbol == "}":  #speed up
                    current_display_speed = max(0, current_display_speed - 1)
                continue
            elif escaped and symbol == "n":  #Newline monstrosity
                continue
            calculated_delay += message_display_speed[current_display_speed]
        return calculated_delay

    def is_iniswap(self, client, preanim, anim, char, sfx):
        """
        Determine if a client is performing an INI swap.
        :param client: client attempting the INI swap.
        :param preanim: name of preanimation
        :param anim: name of idle/talking animation
        :param char: name of character

        """
        if self.iniswap_allowed:
            return False
        if '..' in preanim or '..' in anim or '..' in char:
            # Prohibit relative paths
            return True
        if char.lower() != client.char_name.lower():
            for char_link in self.server.allowed_iniswaps:
                # Only allow if both the original character and the
                # target character are in the allowed INI swap list
                if client.char_name in char_link and char in char_link:
                    return False
        return not self.server.char_emotes[char].validate(preanim, anim, sfx)

    def clear_music(self):
        self.music_list.clear()
        self.music_ref = ''

    def load_music(self, path):
        try:
            with open(path, 'r', encoding='utf-8') as stream:
                music_list = yaml.safe_load(stream)

            prepath = ''
            for item in music_list:
                # deprecated, use 'replace_music' area pref instead
                # if 'replace' in item:
                #     self.replace_music = item['replace'] == True
                if 'use_unique_folder' in item and item[
                        'use_unique_folder'] == True:
                    prepath = os.path.splitext(os.path.basename(path))[0] + '/'

                if 'category' not in item:
                    continue

                if 'songs' in item:
                    for song in item['songs']:
                        song['name'] = prepath + song['name']
            self.music_list = music_list
        except ValueError:
            raise
        except AreaError:
            raise

    def add_jukebox_vote(self, client, music_name, length=-1, showname=''):
        """
        Cast a vote on the jukebox.
        :param music_name: track name
        :param length: length of track (Default value = -1)
        :param showname: showname of voter (?) (Default value = '')
        """
        if not self.jukebox:
            return
        if length == 0:
            self.remove_jukebox_vote(client, False)
            if len(self.jukebox_votes) <= 1 or (not self.music_looper or
                                                self.music_looper.cancelled()):
                self.start_jukebox()
        else:
            if client.change_music_cd():
                client.send_ooc(
                    f'You changed song too many times. Please try again after {int(client.change_music_cd())} seconds.'
                )
                return
            self.remove_jukebox_vote(client, True)
            self.jukebox_votes.append(
                self.JukeboxVote(client, music_name, length, showname))
            client.send_ooc('Your song was added to the jukebox.')
            if len(self.jukebox_votes) == 1 or (not self.music_looper or
                                                self.music_looper.cancelled()):
                self.start_jukebox()

    def remove_jukebox_vote(self, client, silent):
        """
        Removes a vote on the jukebox.
        :param client: client whose vote should be removed
        :param silent: do not notify client

        """
        if not self.jukebox:
            return
        for current_vote in self.jukebox_votes:
            if current_vote.client.id == client.id:
                self.jukebox_votes.remove(current_vote)
        if not silent:
            client.send_ooc('You removed your song from the jukebox.')

    def get_jukebox_picked(self):
        """Randomly choose a track from the jukebox."""
        if not self.jukebox:
            return
        if len(self.jukebox_votes) == 0:
            return None
        elif len(self.jukebox_votes) == 1:
            return self.jukebox_votes[0]
        else:
            weighted_votes = []
            for current_vote in self.jukebox_votes:
                i = 0
                while i < current_vote.chance:
                    weighted_votes.append(current_vote)
                    i += 1
            return random.choice(weighted_votes)

    def start_jukebox(self):
        """Initialize jukebox mode if needed and play the next track."""
        if self.music_looper:
            self.music_looper.cancel()

        # There is a probability that the jukebox feature has been turned off since then,
        # we should check that.
        # We also do a check if we were the last to play a song, just in case.
        if not self.jukebox:
            if self.music_player == 'The Jukebox' and self.music_player_ipid == 'has no IPID':
                self.music = ''
            return

        vote_picked = self.get_jukebox_picked()

        if vote_picked is None:
            self.music = ''
            self.send_command('MC', self.music, -1, '', 1, 0,
                              int(MusicEffect.FADE_OUT))
            return

        if vote_picked.name == self.music:
            return

        self.jukebox_prev_char_id = vote_picked.client.char_id
        if vote_picked.showname == '':
            self.send_command('MC', vote_picked.name,
                              vote_picked.client.char_id, '', 1, 0,
                              int(MusicEffect.FADE_OUT))
        else:
            self.send_command('MC', vote_picked.name,
                              vote_picked.client.char_id, vote_picked.showname,
                              1, 0, int(MusicEffect.FADE_OUT))

        self.music_player = 'The Jukebox'
        self.music_player_ipid = 'has no IPID'
        self.music = vote_picked.name

        for current_vote in self.jukebox_votes:
            # Choosing the same song will get your votes down to 0, too.
            # Don't want the same song twice in a row!
            if current_vote.name == vote_picked.name:
                current_vote.chance = 0
            else:
                current_vote.chance += 1

        length = vote_picked.length
        if length <= 0:  # Length not defined
            length = 120.0  # Play each song for at least 2 minutes

        self.music_looper = asyncio.get_event_loop().call_later(
            max(5, length), lambda: self.start_jukebox())

    def set_ambience(self, name):
        self.ambience = name
        self.send_command(
            'MC', self.ambience, -1, "", 1, 1,
            int(MusicEffect.FADE_OUT | MusicEffect.FADE_IN
                | MusicEffect.SYNC_POS))

    def play_music(self, name, cid, loop=0, showname="", effects=0):
        """
        Play a track.
        :param name: track name
        :param cid: origin character ID
        :param loop: 1 for clientside looping, 0 for no looping (2.8)
        :param showname: showname of origin user
        :param effects: fade out/fade in/sync/etc. effect bitflags
        """
        # If it's anything other than 0, it's looping. (Legacy music.yaml support)
        if loop != 0:
            loop = 1
        self.music_looping = loop
        self.music_effects = effects
        self.send_command('MC', name, cid, showname, loop, 0, effects)

    def can_send_message(self, client):
        """
        Check if a client can send an IC message in this area.
        :param client: sender
        """
        return (time.time() * 1000.0 - self.next_message_time) > 0

    def cannot_ic_interact(self, client):
        """
        Check if this area is muted to a client.
        :param client: sender
        """
        return self.muted and not client.is_mod and not client in self.owners and not client.id in self.invite_list

    def change_hp(self, side, val):
        """
        Set the penalty bars.
        :param side: 1 for defense; 2 for prosecution
        :param val: value from 0 to 10
        """
        if not 0 <= val <= 10:
            raise AreaError('Invalid penalty value.')
        if not 1 <= side <= 2:
            raise AreaError('Invalid penalty side.')
        if side == 1:
            self.hp_def = val
        elif side == 2:
            self.hp_pro = val
        self.send_command('HP', side, val)

    def change_background(self, bg):
        """
        Set the background.
        :param bg: background name
        :raises: AreaError if `bg` is not in background list
        """
        if self.use_backgrounds_yaml:
            if len(self.server.backgrounds) <= 0:
                raise AreaError(
                    'backgrounds.yaml failed to initialize! Please set "use_backgrounds_yaml" to "false" in the config/config.yaml, or create a new "backgrounds.yaml" list in the "config/" folder.'
                )
            if bg.lower() not in (name.lower()
                                  for name in self.server.backgrounds):
                raise AreaError(
                    f'Invalid background name {bg}.\nPlease add it to the "backgrounds.yaml" or change the background name for area [{self.id}] {self.name}.'
                )
        self.background = bg
        for client in self.clients:
            #Update all clients to the pos lock
            if len(self.pos_lock) > 0 and client.pos not in self.pos_lock:
                client.change_position(self.pos_lock[0])
            client.send_command('BN', self.background, client.pos)

    def change_status(self, value):
        """
        Set the status of the area.
        :param value: status code
        """
        allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp',
                          'recess', 'gaming')
        if value.lower() not in allowed_values:
            raise AreaError(
                f'Invalid status. Possible values: {", ".join(allowed_values)}'
            )
        if value.lower() == 'lfp':
            value = 'looking-for-players'
        self.status = value.upper()
        self.area_manager.send_arup_status()

    def change_doc(self, doc='No document.'):
        """
        Set the doc link.
        :param doc: doc link (Default value = 'No document.')
        """
        self.doc = doc

    def add_to_judgelog(self, client, msg):
        """
        Append an event to the judge log (max 10 items).
        :param client: event origin
        :param msg: event message
        """
        if len(self.judgelog) >= 10:
            self.judgelog = self.judgelog[1:]
        self.judgelog.append(f'{client.char_name} ({client.ip}) {msg}.')

    def add_music_playing(self, client, name, showname='', autoplay=None):
        """
        Set info about the current track playing.
        :param client: player
        :param showname: showname of player (can be blank)
        :param name: track name
        :param autoplay: if track will play itself as soon as user joins area
        """
        if showname != '':
            self.music_player = f'{showname} ({client.char_name})'
        else:
            self.music_player = client.char_name
        self.music_player_ipid = client.ipid
        self.music = name
        if autoplay == None:
            autoplay = self.music_autoplay
        self.music_autoplay = autoplay

    def get_evidence_list(self, client):
        """
        Get the evidence list of the area.
        :param client: requester
        """
        client.evi_list, evi_list = self.evi_list.create_evi_list(client)
        if client.blinded:
            return [0]
        return evi_list

    def broadcast_evidence_list(self):
        """
        Broadcast an updated evidence list.
        LE#<name>&<desc>&<img>#<name>
        """
        for client in self.clients:
            client.send_command('LE', *self.get_evidence_list(client))

    def get_owners(self):
        """
        Get a string of area's owners (CMs).
        :return: message
        """
        msg = ''
        for i in self._owners:
            msg += f'[{str(i.id)}] {i.showname}, '
        if len(msg) > 2:
            msg = msg[:-2]
        return msg

    def add_owner(self, client):
        """
        Add a CM to the area.
        """
        self._owners.add(client)

        # Make sure the client's available areas are updated
        self.broadcast_area_list(client)
        self.area_manager.send_arup_cms()
        self.broadcast_evidence_list()

        self.broadcast_ooc(
            f'{client.showname} [{client.id}] is CM in this area now.')

    def remove_owner(self, client, dc=False):
        """
        Remove a CM from the area.
        """
        self._owners.remove(client)
        if not dc and len(client.broadcast_list) > 0:
            client.broadcast_list.clear()
            client.send_ooc('Your broadcast list has been cleared.')

        if self.area_manager.single_cm and len(self._owners) == 0:
            if self.locked:
                self.unlock()
            if self.password != '':
                self.password = ''
            if self.muted:
                self.unmute()
            self.name = self.o_name
            self.doc = self.o_doc
            self.desc = self.o_desc
            self.change_background(self.o_background)
            self.pos_lock.clear()

        if not dc:
            # Make sure the client's available areas are updated
            self.broadcast_area_list(client)
            self.area_manager.send_arup_cms()
            self.broadcast_evidence_list()

        self.broadcast_ooc(
            f'{client.showname} [{client.id}] is no longer CM in this area.')

    def broadcast_area_list(self, client=None, refresh=False):
        """
        Send the accessible and visible areas to the client.
        """
        clients = []
        if client == None:
            clients = list(self.clients)
        else:
            clients.append(client)

        update_clients = []
        for c in clients:
            allowed = c.is_mod or c in self.owners
            area_list = c.get_area_list(allowed, allowed)
            if refresh or c.local_area_list != area_list:
                update_clients.append(c)
                c.reload_area_list(area_list)

        # Update ARUP information only for those that need it
        if len(update_clients) > 0:
            self.area_manager.send_arup_status(update_clients)
            self.area_manager.send_arup_lock(update_clients)
            self.area_manager.send_arup_cms(update_clients)

    def time_until_move(self, client):
        """
        Sum up the movement delays. For example,
        if client has 1s move delay, area has 3s move delay, and hub has 2s move delay,
        the resulting delay will be 1+3+2=6 seconds.
        Negative numbers are allowed.
        :return: time left until you can move again or 0.
        """
        secs = round(time.time() * 1000.0 - client.last_move_time)
        total = sum(
            [client.move_delay, self.move_delay, self.area_manager.move_delay])
        test = total * 1000.0 - secs
        if test > 0:
            return test
        return 0

    @property
    def minigame_time_left(self):
        """Time left on the currently running minigame."""
        if not self.minigame_schedule or self.minigame_schedule.cancelled():
            return 0
        return self.minigame_schedule.when() - asyncio.get_event_loop().time()

    def end_minigame(self):
        if self.minigame_schedule:
            self.minigame_schedule.cancel()

        self.muted = self.old_muted
        self.invite_list = self.old_invite_list
        self.red_team.clear()
        self.blue_team.clear()

        self.send_ic(None, '1', 0, "", "../misc/blank",
                     f"~~{self.minigame} END!", "", "", 0, -1, 0, 0, [0], 0, 0,
                     0, "System", -1, "", "", 0, 0, 0, 0, "0", 0, "", "", "",
                     0, "")
        self.minigame = ''

    def start_debate(self, client, target, pta=False):
        if (client.char_id in self.red_team and target.char_id
                in self.blue_team) or (client.char_id in self.blue_team
                                       and target.char_id in self.red_team):
            raise AreaError("Target is already on the opposing team!")

        if self.minigame == 'Scrum Debate':
            if target.char_id in self.red_team:
                self.red_team.discard(client.char_id)
                self.blue_team.add(client.char_id)
                self.invite_list.add(client.id)
                team = 'blue'
            elif target.char_id in self.blue_team:
                self.blue_team.discard(client.char_id)
                self.red_team.add(client.char_id)
                self.invite_list.add(client.id)
                team = 'red'
            else:
                raise AreaError('Target is not part of the minigame!')

            if len(self.blue_team) <= 0:
                self.broadcast_ooc('Blue team conceded!')
                self.end_minigame()
                return
            elif len(self.red_team) <= 0:
                self.broadcast_ooc('Red team conceded!')
                self.end_minigame()
                return
            self.broadcast_ooc(
                f'[{client.id}] {client.showname} is now part of the {team} team!'
            )
            database.log_area(
                'minigame.sd',
                client,
                client.area,
                target=target,
                message=f'{self.minigame} is now part of the {team} team!')
        elif self.minigame == 'Cross Swords':
            if target == client:
                self.broadcast_ooc(
                    f'[{client.id}] {client.showname} conceded!')
                self.end_minigame()
                return
            timeleft = self.minigame_schedule.when() - asyncio.get_event_loop(
            ).time()
            self.minigame_schedule.cancel()
            self.minigame = 'Scrum Debate'
            timer = timeleft + self.scrum_debate_added_time
        elif self.minigame == '':
            if client == target:
                raise AreaError(
                    'You cannot initiate a minigame against yourself!')
            self.old_invite_list = self.invite_list
            self.old_muted = self.muted

            self.muted = True
            self.invite_list.clear()
            self.invite_list.add(client.id)
            self.invite_list.add(target.id)

            self.red_team.clear()
            self.blue_team.clear()
            self.red_team.add(client.char_id)
            self.blue_team.add(target.char_id)
            if pta:
                self.minigame = 'Panic Talk Action'
                timer = self.panic_talk_action_timer
                database.log_area(
                    'minigame.pta',
                    client,
                    client.area,
                    target=target,
                    message=
                    f'{self.minigame} {client.showname} VS {target.showname}')
            else:
                self.minigame = 'Cross Swords'
                timer = self.cross_swords_timer
                database.log_area(
                    'minigame.cs',
                    client,
                    client.area,
                    target=target,
                    message=
                    f'{self.minigame} {client.showname} VS {target.showname}')
        else:
            if target == client:
                self.broadcast_ooc(
                    f'[{client.id}] {client.showname} conceded!')
                self.end_minigame()
                return
            raise AreaError(
                f'{self.minigame} is happening! You cannot interrupt it.')

        self.minigame_schedule = asyncio.get_event_loop().call_later(
            max(5, timer), lambda: self.end_minigame())
        self.broadcast_ooc(
            f'{self.minigame}! [{client.id}] {client.showname}(RED) VS [{target.id}] {target.showname}(BLUE). You have {int(timer)} seconds.\n/cs <id> to join the debate against target ID.'
        )
        # self.send_ic(None, '1', 0, "", "../misc/blank", f"~~}}}}|{self.minigame}!|\n[{client.id}] ~{client.showname}~ VS [{target.id}] √{target.showname}√\\n{int(timer)} seconds left.", "", "", 0, -1, 0, 0, [0], 0, 0, 0, "System", -1, "", "", 0, 0, 0, 0, "0", 0, "", "", "", 0, "")

    class JukeboxVote:
        """Represents a single vote cast for the jukebox."""
        def __init__(self, client, name, length, showname):
            self.client = client
            self.name = name
            self.length = length
            self.chance = 1
            self.showname = showname