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
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 __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)
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))
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
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: 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))
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
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 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
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.next_message_delay = 100 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() 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 # Timers ID 1 thru 4, (indexes 0 to 3 in area), timer ID 0 is global. self.timers = [AreaManager.Timer() for _ in range(4)] 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
def __init__(self, area_id: int, server: TsuserverDR, parameters: Dict[str, Any]): """ 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.id = area_id self.server = server self.publisher = Publisher(self) self.invite_list = {} 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.current_music_source = '' 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.lurk_length = 0 self._in_zone = None self.noteworthy = False self.name = parameters['area'] self.background = parameters['background'] self.background_tod = parameters['background_tod'] 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.global_allowed = parameters['global_allowed'] 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 self.default_reachable_areas = self.reachable_areas.copy() self.visible_reachable_areas = self.reachable_areas.copy() self.reachable_areas.add(self.name) # Area can always reach itself
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)
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))
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 = {}
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