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 ooc_cmd_evidence(client, arg): """ Use /evidence to read all evidence in the area. Use /evidence [evi_name/id] to read specific evidence. Usage: /evidence [evi_name/id] """ evi_list = client.area.get_evidence_list(client) # Just read all area evidence if arg == "": msg = f"==Evidence in '{client.area.name}'==" for i, evi in enumerate(evi_list): # 0 = name # 1 = desc # 2 = image evi_msg = f"\n💼[{i+1}]: '{evi[0]}'" # (🖼️{evi[2]}) if arg == "" or arg.lower() in evi_msg.lower(): msg += evi_msg msg += "\n\n|| Use /evidence [evi_name/id] to read specific evidence. ||" client.send_ooc(msg) return # Arg is not empty try: evidence = None for i, evi in enumerate(evi_list): if (arg.isnumeric() and int(arg) - 1 == i) or arg.lower() == evi[0].lower(): evidence = evi break if evidence is None: raise AreaError(f"Target evidence not found! (/evidence {arg})") msg = f"==💼[{i+1}]: '{evidence[0]}==" msg += f"\n🖼️Image: {evidence[2]}" msg += f"\n📃Desc:\n{evidence[1]}" msg += "\n\n|| Use /evidence to read all evidence in the area ||" client.send_ooc(msg) except ValueError: raise except (AreaError, ClientError): raise
def ooc_cmd_bg(client, arg: str) -> None: """ Set the background of a room. Usage: /bg <background> """ if len(arg) == 0: client.send_ooc(f'Current background is "{client.area.background}".') return if not client.is_mod and client.area.bg_lock == True: raise AreaError("This area's background is locked") try: client.area.change_background(arg) except AreaError: msg = 'custom/' msg += arg try: client.area.change_cbackground(msg) except AreaError: raise client.area.broadcast_ooc( f'{client.char_name} changed the background to {arg}.') database.log_room('bg', client, client.area, message=arg)
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 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 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_area_by_id(self, area_id: int) -> AreaManager.Area: """ Return the Area object corresponding to the area that has the given ID. Parameters ---------- area_id: int Area ID to look for. Returns ------- AreaManager.Area Area. Raises ------ AreaError If no area has the given ID. """ for area in self.areas: if area.id == area_id: return area raise AreaError('Area not found.')
def get_area_by_name(self, name: str) -> AreaManager.Area: """ Return the Area object corresponding to the area that has the given name. Parameters ---------- name: str Area name to look for. Returns ------- AreaManager.Area Area. Raises ------ AreaError If no area has the given name. """ for area in self.areas: if area.name == name: return area raise AreaError('Area not found.')
def check_move_party(self, party, initiator, new_area): """ * If existing handicap has not expired. * If moving while sneaking to lobby/private area. * If target area has some lock player has no perms for. * If target area is unreachable from the current one. * If target area contains a character from the current party. * If character of member is restricted in the target area LIGHTS 1. No one moves 2. Should be no 3. Party disbands 4. No one moves 5. Affected user switches 6. Party disbands """ new_chars = set() movers = {True: dict(), False: dict()} # Assumes that players will only attempt to move if their sneak status matches that of the # initiator. for member in party.members: error = None # Player will only attempt to move under any of these two conditions # * Player and initiator are not sneaking # * Player and initiator are both sneaking if initiator.is_visible: attempt_move = member.is_visible else: attempt_move = not member.is_visible if attempt_move: try: is_restricted = (member.get_char_name() in new_area.restricted_chars) if is_restricted and not member.is_staff(): error = AreaError('', code='ChArRestrictedChar') raise error new_cid, _ = member.check_change_area( new_area, more_unavail_chars=new_chars) new_chars.add(new_cid) except (ClientError, AreaError) as ex: error = ex new_cid = member.char_id else: new_cid = member.char_id if error: if error.code in [ 'ChArHandicap', 'ChArSneakLobby', 'ChArSneakPrivate', 'ChArUnreachable', 'ChrNoAvailableCharacters' ]: member.send_ooc(error.message) culprit = member.displayname if member != initiator else 'yourself' raise ClientError( 'Unable to move the party due to {}.'.format(culprit)) if error.code in [ 'ChArLocked', 'ChArGMLocked', 'ChArModLocked', 'ChArRestrictedChar', 'ChArInArea' ]: movers[False][member] = new_cid elif error.code is not None: raise error else: movers[True][member] = new_cid else: movers[attempt_move][member] = new_cid return movers
def load(self, path='config/areas.yaml', hub_id=-1): try: with open(path, 'r', encoding='utf-8') as stream: hubs = yaml.safe_load(stream) except: raise AreaError(f'File path {path} is invalid!') if hub_id != -1: try: self.hubs[hub_id].load(hubs[hub_id], destructive=True) except ValueError: raise AreaError( f'Invalid Hub ID {hub_id}! Please contact the server host.' ) return if 'area' in hubs[0]: # Legacy support triggered! Abort operation if len(self.hubs) <= 0: self.hubs.append(AreaManager(self, f'Hub 0')) self.hubs[0].load_areas(hubs) is_dr_hub = False # tsuserverDR conversion hell for i, area in enumerate(hubs): # oh God why did they do it this way if 'reachable_areas' in area: reachable_areas = area['reachable_areas'].split(',') # I hate this for a_name in reachable_areas: a_name = a_name.strip() target_area = self.hubs[0].get_area_by_name( a_name, case_sensitive=True) self.hubs[0].areas[i].link(target_area.id) print( f'[tsuDR conversion] Linking area {self.hubs[0].areas[i].name} to {target_area.name}' ) is_dr_hub = True if 'default_description' in area: self.hubs[0].areas[i].desc = area['default_description'] if 'song_switch_allowed' in area: self.hubs[0].areas[i].can_dj = area['song_switch_allowed'] if is_dr_hub: self.hubs[0].arup_enabled = False print( f'[tsuDR conversion] Setting hub 0 ARUP to False due to TsuserverDR yaml supplied. Please use /save_hub as a mod to adapt the areas.yaml to KFO style.' ) return i = 0 for hub in hubs: while len(self.hubs) < len(hubs): # Make sure that the hub manager contains enough hubs to update with new information self.hubs.append(AreaManager(self, f'Hub {len(self.hubs)}')) while len(self.hubs) > len(hubs): # Clean up excess hubs h = self.hubs.pop() clients = h.clients.copy() for client in clients: client.set_area(self.default_hub().default_area()) self.hubs[i].load(hub) self.hubs[i].o_name = self.hubs[i].name self.hubs[i].o_abbreviation = self.hubs[i].abbreviation i += 1
def get_hub_by_abbreviation(self, abbr): """Get a hub by abbreviation.""" for hub in self.hubs: if hub.abbreviation.lower() == abbr.lower(): return hub raise AreaError('Hub not found.')
def get_hub_by_id(self, num): """Get a hub by ID.""" for hub in self.hubs: if hub.id == num: return hub raise AreaError('Hub not found.')
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 get_area_by_name(self, name): """Get an area by name.""" for area in self.areas: if area.name == name: return area raise AreaError('Area not found.')
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 get_area_by_abbreviation(self, abbr): """Get an area by abbreviation.""" for area in self.areas: if area.abbreviation == abbr: return area raise AreaError('Area not found.')
def validate_contents(self, contents, extra_parameters=None): if extra_parameters is None: extra_parameters = dict() server_character_list = extra_parameters.get('server_character_list', None) server_default_area_description = extra_parameters.get( 'server_default_area_description', '') default_area_parameters = { 'afk_delay': 0, 'afk_sendto': 0, 'background_tod': dict(), 'bglock': False, 'bullet': True, 'cbg_allowed': False, 'change_reachability_allowed': True, 'default_description': server_default_area_description, 'evidence_mod': 'FFA', 'gm_iclock_allowed': True, 'has_lights': True, 'iniswap_allowed': False, 'global_allowed': True, 'lobby_area': False, 'locking_allowed': False, 'private_area': False, 'reachable_areas': '<ALL>', 'restricted_chars': '', 'rollp_allowed': True, 'rp_getarea_allowed': True, 'rp_getareas_allowed': True, 'scream_range': '', 'song_switch_allowed': False, } current_area_id = 0 area_parameters = list() temp_area_names = set() found_uncheckable_restricted_chars = False # Create the areas for item in contents: # Check required parameters if 'area' not in item: info = 'Area {} has no name.'.format(current_area_id) raise AreaError(info) if 'background' not in item: info = 'Area {} has no background.'.format(item['area']) raise AreaError(info) # Prevent reserved area names (it has a special meaning) reserved_names = { '<ALL>', '<REACHABLE_AREAS>', } for name in reserved_names: if item['area'] == name: info = ( 'An area in your area list is called "{name}". This is a reserved ' 'name, so it is not a valid area name. Please change its name and try ' 'again.') raise AreaError(info) # Prevent names that may be interpreted as a directory with . or .. # This prevents sending the client an entry to their music list which may be read as # including a relative directory if Constants.includes_relative_directories(item['area']): info = ( f'Area {item["area"]} could be interpreted as referencing the current or ' f'parent directories, so it is invalid. Please rename the area and try ' f'again.') raise AreaError(info) # Check unset optional parameters for param in default_area_parameters: if param not in item: item[param] = default_area_parameters[param] # Check use of backwards incompatible parameters if 'sound_proof' in item: info = ( 'The sound_proof property was defined for area {}. ' 'Support for sound_proof was removed in favor of scream_range. ' 'Please replace the sound_proof tag with scream_range in ' 'your area list and try again.'.format(item['area'])) raise AreaError(info) # Avoid having areas with the same name if item['area'] in temp_area_names: info = ( 'Two areas have the same name in area list: {}. ' 'Please rename the duplicated areas and try again.'.format( item['area'])) raise AreaError(info) # Check if any of the items were interpreted as Python Nones (maybe due to empty lines) for parameter in item: if item[parameter] is None: info = ( 'Parameter {} is manually undefined for area {}. This can be the case ' 'due to having an empty parameter line in your area list. ' 'Please fix or remove the parameter from the area definition and try ' 'again.'.format(parameter, item['area'])) raise AreaError(info) # Check and fix background tods if needed, as YAML parses this as a list of if item['background_tod'] != dict(): raw_background_tod_map = item['background_tod'] if not isinstance(raw_background_tod_map, dict): info = ( f'Expected background TOD for area {item["area"]} be ' f'one dictionary, found it was of type ' f'{type(raw_background_tod_map).__name__}: {raw_background_tod_map}' ) raise AreaError(info) new_background_tod = dict() if not isinstance(raw_background_tod_map, dict): info = ( f'Expected background TOD for area {item["area"]} be a dictionary, ' f'found it was of type {type(raw_background_tod_map).__name__}: ' f'{raw_background_tod_map}') raise AreaError(info) for (key, value) in raw_background_tod_map.items(): tod_name = str(key) tod_background = str(value) if not tod_name.strip(): info = ( f'TOD name `{tod_name}` invalid for area {item["area"]}. ' f'Make sure the TOD name has non-space characters and try ' f'again.') raise AreaError(info) if ' ' in tod_name: info = ( f'TOD name `{tod_name}` invalid for area {item["area"]}. ' f'Make sure the TOD name has no space characters and try ' f'again.') raise AreaError(info) if '|' in tod_name: info = ( f'TOD name `{tod_name}` contains invalid character |.' f'Make sure the TOD name does not have that character and ' f'try again.') raise AreaError(info) if '|' in tod_background: info = ( f'TOD background `{tod_background}` contains invalid ' f'character |. Make sure the TOD name does not have that ' f'character and try again.') raise AreaError(tod_background) new_background_tod[tod_name] = tod_background item['background_tod'] = new_background_tod area_parameters.append(item.copy()) temp_area_names.add(item['area']) current_area_id += 1 # Check if a reachable area is not an area name # Can only be done once all areas are created for area_item in area_parameters: name = area_item['area'] reachable_areas = Constants.fix_and_setify( area_item['reachable_areas']) scream_range = Constants.fix_and_setify(area_item['scream_range']) restricted_chars = Constants.fix_and_setify( area_item['restricted_chars']) if reachable_areas == {'<ALL>'}: reachable_areas = temp_area_names.copy() if scream_range == {'<ALL>'}: scream_range = temp_area_names.copy() elif scream_range == {'<REACHABLE_AREAS>'}: scream_range = reachable_areas.copy() area_item['reachable_areas'] = reachable_areas area_item['scream_range'] = scream_range area_item['restricted_chars'] = restricted_chars # Make sure no weird areas were set as reachable by players or by screams unrecognized_areas = reachable_areas - temp_area_names if unrecognized_areas: info = ( f'Area {name} has unrecognized areas {unrecognized_areas} defined as ' f'areas a player can reach to. Please rename the affected areas and try ' f'again.') raise AreaError(info) unrecognized_areas = scream_range - temp_area_names if unrecognized_areas: info = ( f'Area {name} has unrecognized areas {unrecognized_areas} defined as ' f'areas screams can reach to. Please rename the affected areas and try ' f'again.') raise AreaError(info) # Make sure only characters that exist are part of the restricted char set if server_character_list is not None: unrecognized_characters = restricted_chars - set( server_character_list) if unrecognized_characters: info = ( f'Area {name} has unrecognized characters {unrecognized_characters} ' f'defined as restricted characters. Please make sure the characters ' f'exist and try again.') raise AreaError(info) elif restricted_chars: found_uncheckable_restricted_chars = True if found_uncheckable_restricted_chars: info = ( 'WARNING: Some areas provided default restricted characters. However, no ' 'server character list was provided, so no checks whether restricted ' 'characters were in the character list of the server were performed.' ) print(info) return area_parameters
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 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 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 __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 get_area_by_id(self, num): """Get an area by ID.""" for area in self.areas: if area.id == num: return area raise AreaError('Area not found.')
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 get_sub(self, name): for area in self.subareas: if area.name == name: return area raise AreaError('Area not found.')
def load_areas(self, area_list_file='config/areas.yaml'): self.area_names = set() current_area_id = 0 temp_areas = list() temp_area_names = set() temp_reachable_area_names = set() # Check if valid area list file try: with open(area_list_file, 'r') as chars: areas = yaml.safe_load(chars) except FileNotFoundError: info = 'Could not find area list file {}'.format(area_list_file) raise FileNotFoundError(info) # Create the areas for item in areas: if 'area' not in item: info = 'Area {} has no name.'.format(current_area_id) raise AreaError(info) if 'background' not in item: info = 'Area {} has no background.'.format(item['area']) raise AreaError(info) if 'bglock' not in item: item['bglock'] = False if 'evidence_mod' not in item: item['evidence_mod'] = 'FFA' if 'locking_allowed' not in item: item['locking_allowed'] = False if 'iniswap_allowed' not in item: item['iniswap_allowed'] = True if 'rp_getarea_allowed' not in item: item['rp_getarea_allowed'] = True if 'rp_getareas_allowed' not in item: item['rp_getareas_allowed'] = True if 'rollp_allowed' not in item: item['rollp_allowed'] = True if 'reachable_areas' not in item: item['reachable_areas'] = '<ALL>' if 'change_reachability_allowed' not in item: item['change_reachability_allowed'] = True if 'gm_iclock_allowed' not in item: item['gm_iclock_allowed'] = True if 'afk_delay' not in item: item['afk_delay'] = 0 if 'afk_sendto' not in item: item['afk_sendto'] = 0 if 'lobby_area' not in item: item['lobby_area'] = False if 'private_area' not in item: item['private_area'] = False if 'scream_range' not in item: item['scream_range'] = '' if 'restricted_chars' not in item: item['restricted_chars'] = '' if 'default_description' not in item: item['default_description'] = self.server.config[ 'default_area_description'] if 'has_lights' not in item: item['has_lights'] = True # Backwards compatibility notice if 'sound_proof' in item: info = ( 'The sound_proof property was defined for area {}. ' 'Support for sound_proof was removed in favor of scream_range. ' 'Please replace the sound_proof tag with scream_range in ' 'your area list and try again.'.format(item['area'])) raise AreaError(info) # Avoid having areas with the same name if item['area'] in temp_area_names: info = ( 'Unexpected duplicated area names in area list: {}. ' 'Please rename the duplicated areas and try again.'.format( item['area'])) raise AreaError(info) temp_areas.append(self.Area(current_area_id, self.server, item)) temp_area_names.add(item['area']) temp_reachable_area_names = temp_reachable_area_names.union( temp_areas[-1].reachable_areas) current_area_id += 1 # Check if a reachable area is not an area name # Can only be done once all areas are created unrecognized_areas = temp_reachable_area_names - temp_area_names - { '<ALL>' } if unrecognized_areas != set(): info = ( 'Unrecognized area names defined as a reachable area in area list file: {}. ' 'Please rename the affected areas and try again.'.format( unrecognized_areas)) raise AreaError(info) # Only once all areas have been created, actually set the corresponding values # Helps avoiding junk area lists if there was an error self.areas = temp_areas self.area_names = temp_area_names # If the default area ID is now past the number of available areas, reset it back to zero if self.server.default_area >= len(self.areas): self.server.default_area = 0 # Move existing clients to new corresponding area (or to default area if their previous area no longer exists) for client in self.server.client_manager.clients: try: new_area = self.get_area_by_name(client.area.name) client.change_area(new_area, override_all=True) client.send_host_message('Moving you to new area {}'.format( new_area.name)) except AreaError: client.change_area(self.default_area(), override_all=True) client.send_host_message( 'Your previous area no longer exists. Moving you to default area {}' .format(client.area.name))
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 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 load_areas(self, area_list_file='config/areas.yaml'): """ Load an area list. Parameters ---------- area_list_file: str, optional Location of the area list to load. Defaults to 'config/areas.yaml'. Raises ------ AreaError If any one of the following conditions are met: * An area has no 'area' or no 'background' tag. * An area uses the deprecated 'sound_proof' tag. * Two areas have the same name. * An area parameter was left deliberately blank as opposed to fully erased. * An area has a passage to an undefined area. FileNotFound If the area list could not be found. """ self.area_names = set() current_area_id = 0 temp_areas = list() temp_area_names = set() temp_reachable_area_names = set() # Check if valid area list file with Constants.fopen(area_list_file, 'r') as chars: areas = Constants.yaml_load(chars) def_param = { 'afk_delay': 0, 'afk_sendto': 0, 'bglock': False, 'bullet': True, 'cbg_allowed': False, 'change_reachability_allowed': True, 'default_description': self.server.config['default_area_description'], 'evidence_mod': 'FFA', 'gm_iclock_allowed': True, 'has_lights': True, 'iniswap_allowed': False, 'lobby_area': False, 'locking_allowed': False, 'private_area': False, 'reachable_areas': '<ALL>', 'restricted_chars': '', 'rollp_allowed': True, 'rp_getarea_allowed': True, 'rp_getareas_allowed': True, 'scream_range': '', 'song_switch_allowed': False, } # Create the areas for item in areas: # Check required parameters if 'area' not in item: info = 'Area {} has no name.'.format(current_area_id) raise AreaError(info) if 'background' not in item: info = 'Area {} has no background.'.format(item['area']) raise AreaError(info) # Check unset optional parameters for param in def_param: if param not in item: item[param] = def_param[param] # Prevent names that may be interpreted as a directory with . or .. # This prevents sending the client an entry to their music list which may be read as # including a relative directory if False: # Constants.includes_relative_directories(item['area']): info = ( f'Area {item["area"]} could be interpreted as referencing the current or ' f'parent directories, so it is invalid. Please rename the area and try ' f'again.') raise AreaError(info) # Check use of backwards incompatible parameters if 'sound_proof' in item: info = ( 'The sound_proof property was defined for area {}. ' 'Support for sound_proof was removed in favor of scream_range. ' 'Please replace the sound_proof tag with scream_range in ' 'your area list and try again.'.format(item['area'])) raise AreaError(info) # Avoid having areas with the same name if item['area'] in temp_area_names: info = ( 'Two areas have the same name in area list: {}. ' 'Please rename the duplicated areas and try again.'.format( item['area'])) raise AreaError(info) # Check if any of the items were interpreted as Python Nones (maybe due to empty lines) for parameter in item: if item[parameter] is None: info = ( 'Parameter {} is manually undefined for area {}. This can be the case ' 'due to having an empty parameter line in your area list. ' 'Please fix or remove the parameter from the area definition and try ' 'again.'.format(parameter, item['area'])) raise AreaError(info) temp_areas.append(self.Area(current_area_id, self.server, item)) temp_area_names.add(item['area']) temp_reachable_area_names |= temp_areas[-1].reachable_areas current_area_id += 1 # Check if a reachable area is not an area name # Can only be done once all areas are created unrecognized_areas = temp_reachable_area_names - temp_area_names - { '<ALL>' } if unrecognized_areas != set(): info = ( 'The following areas were defined as reachable areas of some areas in the ' 'area list file, but were not actually defined as areas: {}. Please rename the ' 'affected areas and try again.'.format(unrecognized_areas)) raise AreaError(info) # Only once all areas have been created, actually set the corresponding values # Helps avoiding junk area lists if there was an error # But first, remove all zones backup_zones = self.server.zone_manager.get_zones() for (zone_id, zone) in backup_zones.items(): self.server.zone_manager.delete_zone(zone_id) for client in zone.get_watchers(): client.send_ooc('Your zone has been automatically deleted.') old_areas = self.areas self.areas = temp_areas self.area_names = temp_area_names # And cancel all existing day cycles for client in self.server.client_manager.clients: try: client.server.tasker.remove_task(client, ['as_day_cycle']) except KeyError: pass # And remove all global IC and global IC prefixes for client in self.server.client_manager.clients: if client.multi_ic: client.send_ooc( 'Due to an area list reload, your global IC was turned off. You ' 'may turn it on again manually.') client.multi_ic = None if client.multi_ic_pre: client.send_ooc( 'Due to an area list reload, your global IC prefix was removed. ' 'You may set it again manually.') client.multi_ic_pre = '' # If the default area ID is now past the number of available areas, reset it back to zero if self.server.default_area >= len(self.areas): self.server.default_area = 0 for area in old_areas: # Decide whether the area still exists or not try: new_area = self.get_area_by_name(area.name) remains = True except AreaError: new_area = self.default_area() remains = False # Move existing clients to new corresponding area (or to default area if their previous # area no longer exists). for client in area.clients.copy(): # Check if current char is available if new_area.is_char_available(client.char_id): new_char_id = client.char_id else: try: new_char_id = new_area.get_rand_avail_char_id() except AreaError: new_char_id = -1 if remains: message = 'Area list reload. Moving you to the new {}.' else: message = ( 'Area list reload. Your previous area no longer exists. Moving you ' 'to the server default area {}.') client.send_ooc(message.format(new_area.name)) client.change_area(new_area, ignore_checks=True, change_to=new_char_id, ignore_notifications=True) # Move parties (independently) for party in area.parties.copy(): party.area = new_area new_area.add_party(party) # Update the server's area list only once everything is successful self.server.old_area_list = self.server.area_list self.server.area_list = area_list_file
def get_hub_by_name(self, name): """Get a hub by name.""" for hub in self.hubs: if hub.name.lower() == name.lower(): return hub raise AreaError('Hub not found.')