def add_to_shoutlog(self, client: ClientManager.Client, msg: str): """ Add a shout message to the shout log of the area. Parameters ---------- client: 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_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 net_cmd_rt(self, args): """ Plays the Testimony/CE animation. RT#<type:string>#% """ if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc('You have been muted by a moderator.') return if not self.validate_net_cmd(args, ArgType.STR): return if not args[0].startswith('testimony'): return self.client.area.send_command('RT', args[0]) self.client.area.add_to_judgelog( self.client, 'used judge button {}.'.format(args[0])) logger.log_server( '[{}]{} used judge button {}.'.format(self.client.area.id, self.client.get_char_name(), args[0]), self.client) self.client.last_active = Constants.get_time()
def _continue_timestep(self): if self._was_terminated: # This code should only run if it takes longer for the timer to be terminated than # the firing interval. return if self._timer_value <= self._min_timer_value: self._timer_value = self._min_timer_value if self._timestep_length < 0: self._was_terminated = True self._on_min_end() return elif self._timer_value >= self._max_timer_value: self._timer_value = self._max_timer_value if self._timestep_length > 0: self._was_terminated = True self._on_max_end() return # This moment represents the instant a timestep resumes from pausing/refreshing # or the very beginning of one. self._just_paused = False self._just_unpaused = False # If the timer is paused, wait _firing_interval seconds again if self._is_paused: return # Else, if a timestep just finished without any interruptions # Reset time spent in timestep and last update to timestep if not self._due_continue_timestep_progress: self._reset_subtimestep_elapsed() self._on_timestep_end() adapted_interval = self._firing_interval-self._time_spent_in_timestep if adapted_interval < 0: adapted_interval = 0 self._due_continue_timestep_progress = False self._task = Constants.create_fragile_task(self._wait_timestep_end(adapted_interval))
def net_cmd_hp(self, args): """ Sets the penalty bar. HP#<type:int>#<new_value:int>#% """ if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc("You have been muted by a moderator") return if not self.validate_net_cmd(args, ArgType.INT, ArgType.INT): return try: self.client.area.change_hp(args[0], args[1]) info = 'changed penalty bar {} to {}.'.format(args[0], args[1]) self.client.area.add_to_judgelog(self.client, info) logger.log_server( '[{}]{} changed HP ({}) to {}'.format( self.client.area.id, self.client.get_char_name(), args[0], args[1]), self.client) except AreaError: return self.client.last_active = Constants.get_time()
def create_task(self, client, args): """ Create a new task for given client with given arguments. Parameters ---------- client: ClientManager.Client Client associated to the task. args: list Arguments of the task. """ # Abort old task if it exists try: old_task = self.get_task(client, args) if not old_task.done() and not old_task.cancelled(): self.cancel_task(old_task) except KeyError: pass async_function = getattr(self, args[0])(client, args[1:]) async_future = Constants.create_fragile_task(async_function) self.client_tasks[client.id][args[0]] = (async_future, args[1:], dict())
def convert_symbol_to_word(mes): if mes is None: return None return Constants.encode_ao_packet([mes])[0]
def play_track(self, name: str, client: ClientManager.Client, raise_if_not_found: bool = False, reveal_sneaked: bool = False, pargs: Dict[str, Any] = 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 : bool, 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 : bool, 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.FileInvalidNameError: If `name` references parent or current directories (e.g. "../hi.mp3") 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 Constants.includes_relative_directories(name): info = f'Music names may not reference parent or current directories: {name}' raise ServerError.FileInvalidNameError(info) try: name, length, source = self.server.get_song_data(name, c=client) except ServerError.MusicNotFoundError: if raise_if_not_found: raise name, length, source = name, -1, '' if 'name' not in pargs: pargs['name'] = name if 'char_id' not in pargs: pargs['char_id'] = client.char_id pargs['showname'] = client.showname # 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 def loop(char_id): for client in self.clients: loop_pargs = pargs.copy() # Overwrite in case char_id changed (e.g., server looping) loop_pargs['char_id'] = char_id client.send_music(**loop_pargs) 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['char_id']) # Record the character name and the track they played. self.current_music_player = client.displayname self.current_music = name self.current_music_source = source 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 validate_contents(self, contents, extra_parameters=None) -> List[Dict[str, Any]]: # Check music list contents is indeed a list if not isinstance(contents, list): msg = (f'Expected the music list to be a list, got a ' f'{type(contents).__name__}: {contents}.') raise ServerError.FileSyntaxError(msg) # Check top level description is ok for (i, item) in enumerate(contents.copy()): if item is None: msg = ( f'Expected all music list items to be defined, but item {i} was not.' ) raise ServerError.FileSyntaxError(msg) if not isinstance(item, dict): msg = ( f'Expected all music list items to be dictionaries, but item ' f'{i}: {item} was not a dictionary.') raise ServerError.FileSyntaxError(msg) if set(item.keys()) != {'category', 'songs'}: msg = ( f'Expected all music list items to have exactly two keys: category and ' f'songs, but item {i} had keys {set(item.keys())}') raise ServerError.FileSyntaxError(msg) category, songs = item['category'], item['songs'] if category is None: msg = ( f'Expected all music list categories to be defined, but category {i} was ' f'not.') raise ServerError.FileSyntaxError(msg) if songs is None: msg = ( f'Expected all music list song descriptions to be defined, but song ' f'description {i} was not.') raise ServerError.FileSyntaxError(msg) if not isinstance(category, (str, float, int, bool, complex)): msg = ( f'Expected all music list category names to be strings or numbers, but ' f'category {i}: {category} was not a string or number.') raise ServerError.FileSyntaxError(msg) if not isinstance(songs, list): msg = ( f'Expected all music list song descriptions to be a list, but ' f'description {i}: {songs} was not a list.') raise ServerError.FileSyntaxError(msg) # Check each song description dictionary is ok for (i, item) in enumerate(contents.copy()): category = item['category'] songs = item['songs'] for (j, song) in enumerate(songs): if song is None: msg = ( f'Expected all music list song descriptions to be defined, but song ' f'description {j} in category {i}: {category} was not defined.' ) raise ServerError.FileSyntaxError(msg) if not isinstance(song, dict): msg = ( f'Expected all music list song descriptions to be dictionaries: but ' f'song description {j} in category {i}: {category} was not a ' f'dictionary: {song}.') raise ServerError.FileSyntaxError(msg) if 'name' not in song.keys(): msg = ( f'Expected all music lists song descriptions to have a name as key, but ' f'song description {j} in category {i}: {category} ' f'had keys {set(song.keys())}.') raise ServerError.FileSyntaxError(msg) if not set(song.keys()).issubset({'name', 'length', 'source'}): msg = ( f'Expected all music list song description keys be contained in the set ' f"{{'name', 'length', 'source'}} but song description {j} in category " f'{i}: {category} had keys {set(song.keys())}.') raise ServerError.FileSyntaxError(msg) name = song['name'] length = song['length'] if 'length' in song else -1 source = song['source'] if 'source' in song else '' if not isinstance(name, (str, float, int, bool, complex)): msg = ( f'Expected all music list song names to be strings or numbers, but ' f'song {j}: {name} in category {i}: {category} was not a string or ' f'number.') raise ServerError.FileSyntaxError(msg) if not isinstance(length, (int, float)): msg = ( f'Expected all music list song lengths to be numbers, but song {j}: ' f'{name} in category {i}: {category} had non-numerical length {length}.' ) raise ServerError.FileSyntaxError(msg) if not isinstance(source, (str, float, int, bool, complex)): msg = ( f'Expected all music list song sources to be strings or numbers, but ' f'song {j}: {name} in category {i}: {category} was not a string or ' f'number.') # 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(name): info = ( f'Music {name} could be interpreted as referencing current or ' f'parent directories, so it is invalid.') raise ServerError.FileSyntaxError(info) return contents
def net_cmd_ct(self, args): """ OOC Message CT#<name:string>#<message:string>#% """ if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc("You have been muted by a moderator.") return if not self.validate_net_cmd( args, ArgType.STR, ArgType.STR, needs_auth=False): return if self.client.name != args[0] and self.client.fake_name != args[0]: if self.client.is_valid_name(args[0]): self.client.name = args[0] self.client.fake_name = args[0] else: self.client.fake_name = args[0] self.client.name = '' if self.client.name == '': self.client.send_ooc( 'You must insert a name with at least one letter.') return if self.client.name.startswith(' '): self.client.send_ooc( 'You must insert a name that starts with a letter.') return if self.server.config[ 'hostname'] in self.client.name or '<dollar>G' in self.client.name: self.client.send_ooc('That name is reserved.') return if args[1].startswith('/'): spl = args[1][1:].split(' ', 1) cmd = spl[0] arg = '' if len(spl) == 2: arg = spl[1][:1024] try: called_function = 'ooc_cmd_{}'.format(cmd) function = None # Double assignment to check if it matched to a function later function = getattr(self.server.commands, called_function) except AttributeError: try: function = getattr(self.server.commands_alt, called_function) except AttributeError: logger.log_print('Attribute error with ' + called_function) self.client.send_ooc('Invalid command.') if function: try: function(self.client, arg) except TsuserverException as ex: self.client.send_ooc(ex) else: # Censor passwords if accidentally said without a slash in OOC for password in self.server.all_passwords: for login in ['login ', 'logincm ', 'loginrp ', 'logingm ']: if login + password in args[1]: args[1] = args[1].replace(password, '[CENSORED]') if self.client.disemvowel: #If you are disemvoweled, replace string. args[1] = Constants.disemvowel_message(args[1]) if self.client.disemconsonant: #If you are disemconsonanted, replace string. args[1] = Constants.disemconsonant_message(args[1]) if self.client.remove_h: #If h is removed, replace string. args[1] = Constants.remove_h_message(args[1]) self.client.area.send_command('CT', self.client.name, args[1]) self.client.last_ooc_message = args[1] logger.log_server( '[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.name, args[1]), self.client) self.client.last_active = Constants.get_time()
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 notify_me_status(self, area: AreaManager.Area, changed_visibility: bool = True, changed_hearing: bool = True) -> bool: client = self.client normal_visibility = changed_visibility and area.lights and not client.is_blind info = '' vis_info = '' sne_info = '' # While we always notify in OOC if someone has a custom status, we only ping IC if the # status has changed from the last time the player has seen it. # This prevents ping spam if two players with custom statuses move together. found_something = False status_visible = [c for c in area.clients if c.status and c != client and c.is_visible] status_sneaking = [c for c in area.clients if c.status and c != client and not c.is_visible] staff_privileged = not normal_visibility if status_visible: if client.is_staff() or normal_visibility: mark = '(X) ' if staff_privileged else '' players = Constants.cjoin([c.displayname for c in status_visible]) verb = 'was' if len(status_visible) == 1 else 'were' vis_info = (f'{mark}You note something about {players} who {verb} in the area ' f'already') for player in status_visible: remembered_status = client.remembered_statuses.get(player.id, '') if player.status != remembered_status: # Found someone whose status has changed # Only for these situations do we want to ping found_something = True client.remembered_statuses[player.id] = player.status elif changed_visibility and not client.is_deaf: # Give nerfed notifications if the lights are out or the player is blind, but NOT # if the player is deaf. vis_info = 'You think something is unusual about someone in the area' # To prepare message with sneaked bleeding, you must be staff. # Otherwise, prepare faint drops of blood if you are not deaf. # Otherwise, just prepare 'smell' if lights turned off or you are blind if status_sneaking: if client.is_staff(): players = Constants.cjoin([c.displayname for c in status_sneaking]) verb = 'was' if len(status_sneaking) == 1 else 'were' sne_info = (f'(X) You note something about {players}, who {verb} in the area ' f'already and also sneaking') if vis_info and sne_info: # Remove marks and capital letters. Use 'count=1' explicitly to prevent more # replacements that could happen with player displaynames. # Then readd the mark manually (mark would have been present because sne_info) vis_info = vis_info[4:] if vis_info.startswith('(X)') else vis_info sne_info = sne_info.replace("(X) You", "you", 1) info = f'(X) {vis_info}, and {sne_info}' elif vis_info: info = vis_info elif sne_info: info = sne_info if info: client.send_ooc(info + '.') return found_something
def data_received(self, data): """ Handles any data received from the network. Receives data, parses them into a command and passes it to the command handler. :param data: bytes of data """ buf = data if buf is None: buf = b'' # try to decode as utf-8, ignore any erroneous characters self.buffer += buf.decode('utf-8', 'ignore') self.buffer = self.buffer.translate({ord(c): None for c in '\0'}) if len(self.buffer) > 8192: msg = self.buffer if len(self.buffer) < 512 else self.buffer[:512] + '...' logger.log_server('Terminated {} (packet too long): sent {} ({} bytes)' .format(self.client.get_ipreal(), msg, len(self.buffer))) self.client.disconnect() return found_message = False for msg in self.get_messages(): found_message = True if len(msg) < 2: # This immediatelly kills any client that does not even try to follow the proper # client protocol msg = self.buffer if len(self.buffer) < 512 else self.buffer[:512] + '...' logger.log_server('Terminated {} (packet too short): sent {} ({} bytes)' .format(self.client.get_ipreal(), msg, len(self.buffer))) self.client.disconnect() return # general netcode structure is not great if msg[0] in ('#', '3', '4'): if msg[0] == '#': msg = msg[1:] raw_parameters = msg.split('#') raw_parameters[0] = fanta_decrypt(raw_parameters[0]) msg = '#'.join(raw_parameters) logger.log_debug('[INC][RAW]{}'.format(msg), self.client) try: if self.server.print_packets: print(f'> {self.client.id}: {msg}') self.server.log_packet(self.client, msg, True) # Decode AO clients' encoding cmd, *args = Constants.decode_ao_packet(msg.split('#')) try: dispatched = self.net_cmd_dispatcher[cmd] except KeyError: logger.log_pserver(f'Client {self.client.id} sent abnormal packet {msg} ' f'(client version: {self.client.version}).') else: dispatched(self, args) except AOProtocolError.InvalidInboundPacketArguments: pass except Exception as ex: self.server.send_error_report(self.client, cmd, args, ex) if not found_message: # This immediatelly kills any client that does not even try to follow the proper # client protocol msg = self.buffer if len(self.buffer) < 512 else self.buffer[:512] + '...' logger.log_server('Terminated {} (packet syntax unrecognized): sent {} ({} bytes)' .format(self.client.get_ipreal(), msg, len(self.buffer))) self.client.disconnect()
def log_error(msg, server, errortype='P') -> str: # errortype "C" if server raised an error as a result of a client packet. # errortype "D" if player manually requested an error dump # errortype "P" if server raised an error for any other reason error_log = logging.getLogger('error') file = 'logs/{}{}.log'.format(Constants.get_time_iso(), errortype) file = file.replace(':', '') error_handler = logging.FileHandler(file, encoding='utf-8') error_handler.setLevel(logging.ERROR) error_handler.setFormatter( logging.Formatter('[%(asctime)s UTC]%(message)s')) error_log.addHandler(error_handler) if server: # Add list of most recent packets msg += f'\n\n\n= {server.logged_packet_limit} most recent packets dump =' if not server.logged_packets: msg += '\nNo logged packets.' else: for logged_packet in server.logged_packets: str_logged_packet = ' '.join(logged_packet) msg += f'\n{str_logged_packet}' # Add list of clients to error log try: msg += '\n\n\n= Client dump. =' msg += '\n*Number of clients: {}'.format(len(server.get_clients())) msg += '\n*Current clients' clients = sorted(server.get_clients(), key=lambda c: c.id) for c in clients: msg += '\n\n{}'.format(c.get_info(as_mod=True)) except Exception: etype, evalue, etraceback = sys.exc_info() msg += '\nError generating client dump.' msg += '\n{}'.format("".join( traceback.format_exception(etype, evalue, etraceback))) # Add list of areas to error log try: msg += '\n\n\n= Area dump =' msg += '\n*Current area list: {}'.format(server.area_list) msg += '\n*Old area list: {}'.format(server.old_area_list) msg += '\n*Current areas:' for area in server.area_manager.areas: msg += '\n**{}'.format(area) for c in area.clients: msg += '\n***{}'.format(c) except Exception: etype, evalue, etraceback = sys.exc_info() msg += '\nError generating area dump.' msg += '\n{}'.format("".join( traceback.format_exception(etype, evalue, etraceback))) else: # Case server was not initialized properly, so areas and clients are not set msg += ( '\nServer was not initialized, so packet, client and area dumps could not be ' 'generated.') # Write and log error_log.error(msg) error_log.removeHandler(error_handler) log_pserver('Successfully created server dump file {}'.format(file)) return file
def log_print(msg, client=None): msg = f'{parse_client_info(client)}{msg}' current_time = Constants.get_time_iso() print('{}: {}'.format(current_time, msg))
def net_cmd_ms(self, args): """ IC message. Refer to the implementation for details. """ if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc("You have been muted by a moderator.") return if self.client.area.ic_lock and not self.client.is_staff(): self.client.send_ooc( "IC chat in this area has been locked by a moderator.") return if not self.client.area.can_send_message(): return pargs = self.process_arguments('ms', args) if not pargs: return if not self.client.area.iniswap_allowed: if self.client.area.is_iniswap(self.client, pargs['pre'], pargs['anim'], pargs['folder']): self.client.send_ooc("Iniswap is blocked in this area.") return if pargs[ 'folder'] in self.client.area.restricted_chars and not self.client.is_staff( ): self.client.send_ooc('Your character is restricted in this area.') return if pargs['msg_type'] not in ('chat', '0', '1'): return if pargs['anim_type'] not in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10): return if pargs['cid'] != self.client.char_id: return if pargs['sfx_delay'] < 0: return if pargs['button'] not in (0, 1, 2, 3, 4, 5, 6, 7): # Shouts return if pargs[ 'button'] > 0 and not self.client.area.bullet and not self.client.is_staff( ): self.client.send_ooc('Bullets are disabled in this area.') return if pargs['evidence'] < 0: return if pargs['ding'] not in (0, 1, 2, 3, 4, 5, 6, 7): # Effects return if pargs['color'] not in (0, 1, 2, 3, 4, 5, 6): return if pargs[ 'color'] == 5 and not self.client.is_mod and not self.client.is_cm: pargs['color'] = 0 if pargs['color'] == 6: # Remove all unicode to prevent now yellow text abuse pargs['text'] = re.sub(r'[^\x00-\x7F]+', ' ', pargs['text']) if len(pargs['text'].strip(' ')) == 1: pargs['color'] = 0 else: if pargs['text'].strip(' ') in ('<num>', '<percent>', '<dollar>', '<and>'): pargs['color'] = 0 if self.client.pos: pargs['pos'] = self.client.pos else: if pargs['pos'] not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): return self.client.pos = pargs['pos'] # Truncate and alter message if message effect is in place raw_msg = pargs['text'][:256] msg = raw_msg if self.client.gimp: #If you are gimped, gimp message. msg = Constants.gimp_message() if self.client.disemvowel: #If you are disemvoweled, replace string. msg = Constants.disemvowel_message(msg) if self.client.disemconsonant: #If you are disemconsonanted, replace string. msg = Constants.disemconsonant_message(msg) if self.client.remove_h: #If h is removed, replace string. msg = Constants.remove_h_message(msg) gag_replaced = False if self.client.is_gagged: allowed_starters = ('(', '*', '[') if msg != ' ' and not msg.startswith(allowed_starters): gag_replaced = True msg = Constants.gagged_message() if msg != raw_msg: self.client.send_ooc_others( '(X) {} tried to say `{}` but is currently gagged.'.format( self.client.displayname, raw_msg), is_zstaff_flex=True, in_area=True) if pargs['evidence']: evidence_position = self.client.evi_list[pargs['evidence']] - 1 if self.client.area.evi_list.evidences[ evidence_position].pos != 'all': self.client.area.evi_list.evidences[ evidence_position].pos = 'all' self.client.area.broadcast_evidence_list() # If client has GlobalIC enabled, set area range target to intended range and remove # GlobalIC prefix if needed. if self.client.multi_ic is None or not msg.startswith( self.client.multi_ic_pre): area_range = range(self.client.area.id, self.client.area.id + 1) else: # As msg.startswith('') is True, this also accounts for having no required prefix. start, end = self.client.multi_ic[ 0].id, self.client.multi_ic[1].id + 1 start_area = self.server.area_manager.get_area_by_id(start) end_area = self.server.area_manager.get_area_by_id(end - 1) area_range = range(start, end) msg = msg.replace(self.client.multi_ic_pre, '', 1) if start != end - 1: self.client.send_ooc( 'Sent global IC message "{}" to areas {} through {}.'. format(msg, start_area.name, end_area.name)) else: self.client.send_ooc( 'Sent global IC message "{}" to area {}.'.format( msg, start_area.name)) pargs['msg'] = msg pargs['evidence'] = self.client.evi_list[pargs['evidence']] pargs[ 'showname'] = '' # Dummy value, actual showname is computed later # Compute pairs # Based on tsuserver3.3 code # Only do this if character is paired, which would only happen for AO 2.6 clients self.client.charid_pair = pargs[ 'charid_pair'] if 'charid_pair' in pargs else -1 self.client.offset_pair = pargs[ 'offset_pair'] if 'offset_pair' in pargs else 0 self.client.flip = pargs['flip'] self.client.char_folder = pargs['folder'] if pargs['anim_type'] not in (5, 6): self.client.last_sprite = pargs['anim'] pargs['other_offset'] = 0 pargs['other_emote'] = 0 pargs['other_flip'] = 0 pargs['other_folder'] = '' if 'charid_pair' not in pargs or pargs['charid_pair'] < -1: pargs['charid_pair'] = -1 if pargs['charid_pair'] > -1: for target in self.client.area.clients: if target == self.client: continue # Check pair has accepted pair if target.char_id != self.client.charid_pair: continue if target.charid_pair != self.client.char_id: continue # Check pair is in same position if target.pos != self.client.pos: continue pargs['other_offset'] = target.offset_pair pargs['other_emote'] = target.last_sprite pargs['other_flip'] = target.flip pargs['other_folder'] = target.char_folder break else: # There are no clients who want to pair with this client pargs['charid_pair'] = -1 for area_id in area_range: target_area = self.server.area_manager.get_area_by_id(area_id) for c in target_area.clients: c.send_ic(params=pargs, sender=self.client, gag_replaced=gag_replaced) target_area.set_next_msg_delay(len(msg)) # Deal with shoutlog if pargs['button'] > 0: info = 'used shout {} with the message: {}'.format( pargs['button'], msg) target_area.add_to_shoutlog(self.client, info) self.client.area.set_next_msg_delay(len(msg)) logger.log_server( '[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client) # Sending IC messages reveals sneaked players if not self.client.is_staff() and not self.client.is_visible: self.client.change_visibility(True) self.client.send_ooc_others( '(X) {} revealed themselves by talking ({}).'.format( self.client.displayname, self.client.area.id), is_zstaff=True) self.server.tasker.create_task(self.client, [ 'as_afk_kick', self.client.area.afk_delay, self.client.area.afk_sendto ]) if self.client.area.is_recording: self.client.area.recorded_messages.append(args) self.client.last_ic_message = msg self.client.last_active = Constants.get_time()
def convert_word_to_symbol(mes): if mes is None: return None return Constants.decode_ao_packet([mes])[0]
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 new_group(self, playergroup_type=None, creator=None, player_limit=None, player_concurrent_limit=1, require_invitations=False, require_players=True, require_leaders=True): """ Create a new player group managed by this manager. Parameters ---------- playergroup_type : PlayerGroup Class of player group that will be produced. Defaults to None (and converted to the default player group created by this player group manager). creator : ClientManager.Client, optional The player who created this group. If set, they will also be added to the group. Defaults to None. player_limit : int or None, optional If an int, it is the maximum number of players the player group supports. If None, it indicates the player group has no player limit. Defaults to None. player_concurrent_limit : int or None, optional If an int, it is the maximum number of player groups managed by `self` that any player of this group to create may belong to, including this group to create. If None, it indicates that this group does not care about how many other player groups managed by `self` each of its players belongs to. Defaults to 1 (a player may not be in another group managed by `self` while in this new group). require_invitations : bool, optional If True, users can only be added to the group if they were previously invited. If False, no checking for invitations is performed. Defaults to False. require_players : bool, optional If True, if at any point the group loses all its players, the group will automatically be deleted. If False, no such automatic deletion will happen. Defaults to True. require_leaders : bool, optional If True, if at any point the group has no leaders left, the group will choose a leader among any remaining players left; if no players are left, the next player added will be made leader. If False, no such automatic assignment will happen. Defaults to True. Returns ------- PlayerGroup The created player group. Raises ------ PlayerGroupError.ManagerTooManyGroupsError If the manager is already managing its maximum number of groups. PlayerGroupError.UserHitGroupConcurrentLimitError. If `creator` has reached the concurrent player membership limit of any of the groups it belongs to managed by this manager, or by virtue of joining this group the creator they will violate this group's concurrent player membership limit. """ if self._playergroup_limit is not None: if len(self._id_to_group) >= self._playergroup_limit: raise PlayerGroupError.ManagerTooManyGroupsError if creator: # Check if adding the creator to this new group would cause any concurrent # membership limits being reached. if self.find_player_concurrent_limiting_group(creator): raise PlayerGroupError.UserHitGroupConcurrentLimitError groups_of_user = self._user_to_groups.get(creator, None) if groups_of_user is not None and len(groups_of_user) >= player_concurrent_limit: raise PlayerGroupError.UserHitGroupConcurrentLimitError # At this point, we are committed to creating this player group. # Generate a playergroup ID and the new group def_args = ( self._server, self, self.get_available_group_id(), ) def_kwargs = { 'player_limit': player_limit, 'player_concurrent_limit': player_concurrent_limit, 'require_invitations': require_invitations, 'require_players': require_players, 'require_leaders': require_leaders, } new_playergroup_type = Constants.make_partial_from(playergroup_type, self._default_playergroup_type, *def_args, **def_kwargs) playergroup = new_playergroup_type() playergroup_id = playergroup.get_id() self._id_to_group[playergroup_id] = playergroup if creator: playergroup.add_player(creator) self._check_structure() return playergroup
def notify_me_blood(self, area, changed_visibility=True, changed_hearing=True): client = self.client changed_area = (client.area != area) ########### # If someone else is bleeding in the new area, notify the person moving bleeding_visible = [ c for c in area.clients if c.is_visible and c.is_bleeding and c != client ] bleeding_sneaking = [ c for c in area.clients if not c.is_visible and c.is_bleeding and c != client ] info = '' vis_info = '' sne_info = '' # To prepare message with players bleeding, one of these must be true: # 1. You are staff # 2. Lights are on and you are not blind # Otherwise, prepare faint drops of blood if you are not deaf. # Otherwise, just prepare 'smell' if lights turned off or you are blind if bleeding_visible: normal_visibility = changed_visibility and area.lights and not client.is_blind if client.is_staff() or normal_visibility: vis_info = ('{}You see {} {} bleeding'.format( '(X) ' if not normal_visibility else '', Constants.cjoin([c.displayname for c in bleeding_visible]), 'is' if len(bleeding_visible) == 1 else 'are')) elif not client.is_deaf and changed_hearing: vis_info = 'You hear faint drops of blood' elif client.is_blind and client.is_deaf and changed_area: vis_info = 'You smell blood' # To prepare message with sneaked bleeding, you must be staff. # Otherwise, prepare faint drops of blood if you are not deaf. # Otherwise, just prepare 'smell' if lights turned off or you are blind if bleeding_sneaking: if client.is_staff(): sne_info = ('(X) You see {} {} bleeding while sneaking'.format( Constants.cjoin([c.displayname for c in bleeding_sneaking]), 'is' if len(bleeding_visible) == 1 else 'are')) elif not client.is_deaf and changed_hearing: sne_info = 'You hear faint drops of blood' elif not area.lights or client.is_blind and changed_area: sne_info = 'You smell blood' # If there is visible info, merge it with sneak info if the following is true # 1. There is sneak info # 2. Sneak info is not 'You smell blood' (as that would be true anyway) # 3. It is not the same as the visible info (To avoid double 'hear faint drops') if vis_info: if sne_info and sne_info != 'You smell blood' and vis_info != sne_info: info = '{}, and {}'.format(info, sne_info.lower()) else: info = vis_info else: info = sne_info if info: client.send_ooc(info + '.') ########### # If there are blood trails in the area, send notification if one of the following is true ## 1. You are staff ## 2. Lights are on and you are not blind. ## If the blood in the area is smeared, just indicate there is smeared blood for non-staff ## and the regular blood trail message with extra text for staff. # If neither is true, send 'smell' notification as long as the following is true: # 1. Lights turned off or you are blind # 2. A notification was not sent in the previous part normal_visibility = changed_visibility and area.lights and not client.is_blind if client.is_staff() or normal_visibility: start_connector = '(X) ' if not normal_visibility else '' smeared_connector = 'smeared ' if client.is_staff( ) and area.blood_smeared else '' if not client.is_staff() and area.blood_smeared: client.send_ooc( '{}You spot some smeared blood in the area.'.format( start_connector)) elif area.bleeds_to == set([area.name]): client.send_ooc('{}You spot some {}blood in the area.'.format( start_connector, smeared_connector)) elif len(area.bleeds_to) > 1: bleed_to_areas = list(area.bleeds_to - set([area.name])) if client.is_staff() and area.blood_smeared: start_connector = '(X) ' # Force staff indication info = ('{}You spot a {}blood trail leading to {}.'.format( start_connector, smeared_connector, Constants.cjoin(bleed_to_areas, the=True))) client.send_ooc(info) elif not client.is_staff() and (area.bleeds_to or area.blood_smeared) and changed_area: if not info: client.send_ooc('You smell blood.')
def check_change_area(self, area, override_passages=False, override_effects=False, more_unavail_chars=None): """ Perform all checks that would prevent an area change. Right now there is, (in this order) * In target area already. * 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 no available characters in the new area. ** In this check a new character is selected if there is a character conflict too. However, the change is not performed in this portion of code. No send_oocs commands are meant to be put here, so as to avoid unnecessary notifications. Append any intended messages to the captured_messages list and then manually send them out outside this function. """ client = self.client captured_messages = list() # Obvious check first if client.area == area: raise ClientError('User is already in target area.', code='ChArInArea') # Check if player has waited a non-zero movement delay if not client.is_staff( ) and client.is_movement_handicapped and not override_effects: start, length, name, _ = client.server.tasker.get_task_args( client, ['as_handicap']) _, remain_text = Constants.time_remaining(start, length) raise ClientError( "You are still under the effects of movement handicap '{}'. " "Please wait {} before changing areas.".format( name, remain_text), code='ChArHandicap') # Check if trying to move to a lobby/private area while sneaking if area.lobby_area and not client.is_visible and not client.is_mod and not client.is_cm: raise ClientError( 'Lobby areas do not let non-authorized users remain sneaking. Please ' 'change music, speak IC or ask a staff member to reveal you.', code='ChArSneakLobby') if area.private_area and not client.is_visible: raise ClientError( 'Private areas do not let sneaked users in. Please change the ' 'music, speak IC or ask a staff member to reveal you.', code='ChArSneakPrivate') # Check if area has some sort of lock if not client.ipid in area.invite_list: if area.is_locked and not client.is_staff(): raise ClientError('That area is locked.', code='ChArLocked') if area.is_gmlocked and not client.is_mod and not client.is_gm: raise ClientError('That area is gm-locked.', code='ChArGMLocked') if area.is_modlocked and not client.is_mod: raise ClientError('That area is mod-locked.', code='ChArModLocked') # Check if trying to reach an unreachable area if not (client.is_staff() or client.is_transient or override_passages or area.name in client.area.reachable_areas or '<ALL>' in client.area.reachable_areas): info = ( 'Selected area cannot be reached from your area without authorization. ' 'Try one of the following areas instead: ') if client.area.reachable_areas == {client.area.name}: info += '\r\n*No areas available.' else: get_name = client.server.area_manager.get_area_by_name try: sorted_areas = sorted(client.area.reachable_areas, key=lambda x: get_name(x).id) for reachable_area in sorted_areas: if reachable_area != client.area.name: area_id = client.server.area_manager.get_area_by_name( reachable_area).id info += '\r\n*({}) {}'.format( area_id, reachable_area) except AreaError: #When would you ever execute this piece of code is beyond me, but meh info += '\r\n<ALL>' raise ClientError(info, code='ChArUnreachable') # Check if current character is taken in the new area new_char_id = client.char_id if not area.is_char_available(client.char_id, allow_restricted=client.is_staff(), more_unavail_chars=more_unavail_chars): try: new_char_id = area.get_rand_avail_char_id( allow_restricted=client.is_staff(), more_unavail_chars=more_unavail_chars) except AreaError: raise ClientError('No available characters in that area.', code='ChArNoCharacters') return new_char_id, captured_messages
def load_backgrounds(self): with Constants.fopen('config/backgrounds.yaml', 'r', encoding='utf-8') as bgs: self.backgrounds = Constants.yaml_load(bgs)
def net_cmd_ct(self, args: List[str]): """ OOC Message CT#<name:string>#<message:string>#% """ pargs = self.process_arguments('CT', args) username, message = pargs['username'], pargs['message'] # Trim out any leading/trailing whitespace characters up to a chain of spaces username = Constants.trim_extra_whitespace(username) message = Constants.trim_extra_whitespace(message) if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc("You have been muted by a moderator.") return if username == '' or not self.client.is_valid_name(username): self.client.send_ooc('You must insert a name with at least one letter.') return if username.startswith(' '): self.client.send_ooc('You must insert a name that starts with a letter.') return if Constants.contains_illegal_characters(username): self.client.send_ooc('Your name contains an illegal character.') return if (Constants.decode_ao_packet([self.server.config['hostname']])[0] in username or '$G' in username): self.client.send_ooc('That name is reserved.') return # After this the name is validated self.client.publish_inbound_command('CT', pargs) self.client.name = username if message.startswith('/'): spl = message[1:].split(' ', 1) cmd = spl[0] arg = '' if len(spl) == 2: arg = spl[1][:1024] arg = Constants.trim_extra_whitespace(arg) # Do it again because args may be weird try: called_function = 'ooc_cmd_{}'.format(cmd) function = None # Double assignment to check if it matched to a function later function = getattr(self.server.commands, called_function) except AttributeError: try: function = getattr(self.server.commands_alt, called_function) except AttributeError: self.client.send_ooc(f'Invalid command `{cmd}`.') if function: try: function(self.client, arg) except TsuserverException as ex: if ex.message: self.client.send_ooc(ex) else: self.client.send_ooc(type(ex).__name__) else: # Censor passwords if accidentally said without a slash in OOC for password in self.server.all_passwords: for login in ['login ', 'logincm ', 'loginrp ', 'logingm ']: if login + password in args[1]: message = message.replace(password, '[CENSORED]') if self.client.disemvowel: # If you are disemvoweled, replace string. message = Constants.disemvowel_message(message) if self.client.disemconsonant: # If you are disemconsonanted, replace string. message = Constants.disemconsonant_message(message) if self.client.remove_h: # If h is removed, replace string. message = Constants.remove_h_message(message) for client in self.client.area.clients: client.send_ooc(message, username=self.client.name) self.client.last_ooc_message = args[1] logger.log_server('[OOC][{}][{}][{}]{}' .format(self.client.area.id, self.client.get_char_name(), self.client.name, message), self.client) self.client.last_active = Constants.get_time()
def load_config(self): with Constants.fopen('config/config.yaml', 'r', encoding='utf-8') as cfg: self.config = Constants.yaml_load(cfg) self.config['motd'] = self.config['motd'].replace('\\n', ' \n') self.all_passwords = list() # Mandatory passwords must be present in the configuration file. If they are not, # a server error will be raised. mandatory_passwords = ['modpass', 'cmpass', 'gmpass'] for password in mandatory_passwords: if not (password not in self.config or not str(self.config[password])): self.all_passwords.append(self.config[password]) else: err = (f'Password "{password}" is not defined in server/config.yaml. Please ' f'make sure it is set and try again.') raise ServerError(err) # Daily (and guard) passwords are handled differently. They may optionally be left # blank or be not available. What this means is the server does not want a daily # password for that day (or a guard password) optional_passwords = ['guardpass'] + [f'gmpass{i}' for i in range(1, 8)] for password in optional_passwords: if not (password not in self.config or not str(self.config[password])): self.all_passwords.append(self.config[password]) else: self.config[password] = None # Default values to fill in config.yaml if not present defaults_for_tags = { 'utc_offset': 'local', 'discord_link': None, 'max_numdice': 20, 'max_numfaces': 11037, 'max_modifier_length': 12, 'max_acceptable_term': 22074, 'def_numdice': 1, 'def_numfaces': 6, 'def_modifier': '', 'blackout_background': 'Blackout_HD', 'default_area_description': 'No description.', 'party_lights_timeout': 10, 'show_ms2-prober': True, 'showname_max_length': 30, 'sneak_handicap': 5, 'spectator_name': 'SPECTATOR', 'music_change_floodguard': {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0}} for (tag, value) in defaults_for_tags.items(): if tag not in self.config: self.config[tag] = value # Check that all passwords were generated are unique passwords = ['guardpass', 'modpass', 'cmpass', 'gmpass', 'gmpass1', 'gmpass2', 'gmpass3', 'gmpass4', 'gmpass5', 'gmpass6', 'gmpass7'] for (i, password1) in enumerate(passwords): for (j, password2) in enumerate(passwords): if i != j and self.config[password1] == self.config[password2] != None: info = ('Passwords "{}" and "{}" in server/config.yaml match. ' 'Please change them so they are different and try again.' .format(password1, password2)) raise ServerError(info)
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 load_commandhelp(self): with Constants.fopen('README.md', 'r', encoding='utf-8') as readme: lines = [x.rstrip() for x in readme.readlines()] self.linetorank = { '### User Commands': 'normie', '### GM Commands': 'gm', '### Community Manager Commands': 'cm', '### Moderator Commands': 'mod'} self.commandhelp = { 'normie': dict(), 'gm': dict(), 'cm': dict(), 'mod': dict()} # Look for the start of the command list try: start_index = lines.index('## Commands') end_index = lines.index('### Debug commands') except ValueError as error: error_mes = ", ".join([str(s) for s in error.args]) message = ('Unable to generate help based on README.md: {}. Are you sure you have the ' 'latest README.md?'.format(error_mes)) raise ServerError(message) rank = None current_command = None for line in lines[start_index:end_index]: # Check if empty line if not line: continue # Check if this line defines the rank we are taking a look at right now if line in self.linetorank.keys(): rank = self.linetorank[line] current_command = None continue # Otherwise, check if we do not have a rank yet if rank is None: continue # Otherwise, check if this is the start of a command if line[0] == '*': # Get the command name command_split = line[4:].split('** ') if len(command_split) == 1: # Case: * **version** current_command = command_split[0][:-2] else: # Case: * **uninvite** "ID/IPID" current_command = command_split[0] formatted_line = '/{}'.format(line[2:]) formatted_line = formatted_line.replace('**', '') self.commandhelp[rank][current_command] = [formatted_line] continue # Otherwise, line is part of command description, so add it to its current command desc # - Unlocks your area, provided the lock came as a result of /lock. # ... assuming we have a command if current_command: self.commandhelp[rank][current_command].append(line[4:]) continue # Otherwise, we have a line that is a description of the rank # Do nothing about them continue # Not really needed, but made explicit
def net_cmd_ms(self, args): """ IC message. Refer to the implementation for details. """ if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc("You have been muted by a moderator.") return if self.client.area.ic_lock and not self.client.is_staff(): self.client.send_ooc( "IC chat in this area has been locked by a moderator.") return if not self.client.area.can_send_message(): return pargs = self.process_arguments('ms', args) if not pargs: return # First, check if the player just sent the same message with the same character and did # not receive any other messages in the meantime. # This helps prevent record these messages and retransmit it to clients who may want to # filter these out if (pargs['text'] == self.client.last_ic_raw_message and self.client.last_ic_received_mine and self.client.get_char_name() == self.client.last_ic_char): return if not self.client.area.iniswap_allowed: if self.client.area.is_iniswap(self.client, pargs['pre'], pargs['anim'], pargs['folder']): self.client.send_ooc("Iniswap is blocked in this area.") return if pargs[ 'folder'] in self.client.area.restricted_chars and not self.client.is_staff( ): self.client.send_ooc('Your character is restricted in this area.') return if pargs['msg_type'] not in ('chat', '0', '1'): return if pargs['anim_type'] not in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10): return if pargs['cid'] != self.client.char_id: return if pargs['sfx_delay'] < 0: return if pargs['button'] not in (0, 1, 2, 3, 4, 5, 6, 7): # Shouts return if pargs[ 'button'] > 0 and not self.client.area.bullet and not self.client.is_staff( ): self.client.send_ooc('Bullets are disabled in this area.') return if pargs['evidence'] < 0: return if pargs['ding'] not in (0, 1, 2, 3, 4, 5, 6, 7): # Effects return if pargs['color'] not in (0, 1, 2, 3, 4, 5, 6): return if pargs[ 'color'] == 5 and not self.client.is_mod and not self.client.is_cm: pargs['color'] = 0 if pargs['color'] == 6: # Remove all unicode to prevent now yellow text abuse pargs['text'] = re.sub(r'[^\x00-\x7F]+', ' ', pargs['text']) if len(pargs['text'].strip(' ')) == 1: pargs['color'] = 0 else: if pargs['text'].strip(' ') in ('<num>', '<percent>', '<dollar>', '<and>'): pargs['color'] = 0 if self.client.pos: pargs['pos'] = self.client.pos else: if pargs['pos'] not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): return self.client.pos = pargs['pos'] # At this point, the message is guaranteed to be sent # First, update last raw message sent *before* any transformations. That is so that the # server can accurately ignore client sending the same message over and over again self.client.last_ic_raw_message = pargs['text'] self.client.last_ic_char = self.client.get_char_name() # Truncate and alter message if message effect is in place raw_msg = pargs['text'][:256] msg = raw_msg if self.client.gimp: #If you are gimped, gimp message. msg = Constants.gimp_message() if self.client.disemvowel: #If you are disemvoweled, replace string. msg = Constants.disemvowel_message(msg) if self.client.disemconsonant: #If you are disemconsonanted, replace string. msg = Constants.disemconsonant_message(msg) if self.client.remove_h: #If h is removed, replace string. msg = Constants.remove_h_message(msg) gag_replaced = False if self.client.is_gagged: allowed_starters = ('(', '*', '[') if msg != ' ' and not msg.startswith(allowed_starters): gag_replaced = True msg = Constants.gagged_message() if msg != raw_msg: self.client.send_ooc_others( '(X) {} [{}] tried to say `{}` but is currently gagged.'. format(self.client.displayname, self.client.id, raw_msg), is_zstaff_flex=True, in_area=True) # Censor passwords if login command accidentally typed in IC for password in self.server.all_passwords: for login in ['login ', 'logincm ', 'loginrp ', 'logingm ']: if login + password in msg: msg = msg.replace(password, '[CENSORED]') if pargs['evidence']: evidence_position = self.client.evi_list[pargs['evidence']] - 1 if self.client.area.evi_list.evidences[ evidence_position].pos != 'all': self.client.area.evi_list.evidences[ evidence_position].pos = 'all' self.client.area.broadcast_evidence_list() # If client has GlobalIC enabled, set area range target to intended range and remove # GlobalIC prefix if needed. if self.client.multi_ic is None or not msg.startswith( self.client.multi_ic_pre): area_range = range(self.client.area.id, self.client.area.id + 1) else: # As msg.startswith('') is True, this also accounts for having no required prefix. start, end = self.client.multi_ic[ 0].id, self.client.multi_ic[1].id + 1 start_area = self.server.area_manager.get_area_by_id(start) end_area = self.server.area_manager.get_area_by_id(end - 1) area_range = range(start, end) truncated_msg = msg.replace(self.client.multi_ic_pre, '', 1) if start != end - 1: self.client.send_ooc( 'Sent global IC message "{}" to areas {} through {}.'. format(truncated_msg, start_area.name, end_area.name)) else: self.client.send_ooc( 'Sent global IC message "{}" to area {}.'.format( truncated_msg, start_area.name)) pargs['msg'] = msg pargs['evidence'] = self.client.evi_list[pargs['evidence']] pargs[ 'showname'] = '' # Dummy value, actual showname is computed later # Compute pairs # Based on tsuserver3.3 code # Only do this if character is paired, which would only happen for AO 2.6+ clients # Handle AO 2.8 logic # AO 2.8 sends their charid_pair in slightly longer format (\d+\^\d+) # The first bit corresponds to the proper charid_pair, the latter one to whether # the character should appear in front or behind the pair. We still want to extract # charid_pair so pre-AO 2.8 still see the pair; but make it so that AO 2.6 can send pair # messages. Thus, we 'invent' the missing arguments based on available info. if 'charid_pair_pair_order' in pargs: # AO 2.8 sender pargs['charid_pair'] = int( pargs['charid_pair_pair_order'].split('^')[0]) elif 'charid_pair' in pargs: # AO 2.6 sender pargs['charid_pair_pair_order'] = f'{pargs["charid_pair"]}^0' else: # E.g. DRO pargs['charid_pair'] = -1 pargs['charid_pair_pair_order'] = -1 self.client.charid_pair = pargs[ 'charid_pair'] if 'charid_pair' in pargs else -1 self.client.offset_pair = pargs[ 'offset_pair'] if 'offset_pair' in pargs else 0 self.client.flip = pargs['flip'] self.client.char_folder = pargs['folder'] if pargs['anim_type'] not in (5, 6): self.client.last_sprite = pargs['anim'] pargs['other_offset'] = 0 pargs['other_emote'] = 0 pargs['other_flip'] = 0 pargs['other_folder'] = '' if 'charid_pair' not in pargs or pargs['charid_pair'] < -1: pargs['charid_pair'] = -1 pargs['charid_pair_pair_order'] = -1 if pargs['charid_pair'] > -1: for target in self.client.area.clients: if target == self.client: continue # Check pair has accepted pair if target.char_id != self.client.charid_pair: continue if target.charid_pair != self.client.char_id: continue # Check pair is in same position if target.pos != self.client.pos: continue pargs['other_offset'] = target.offset_pair pargs['other_emote'] = target.last_sprite pargs['other_flip'] = target.flip pargs['other_folder'] = target.char_folder break else: # There are no clients who want to pair with this client pargs['charid_pair'] = -1 pargs['offset_pair'] = 0 pargs['charid_pair_pair_order'] = -1 for area_id in area_range: target_area = self.server.area_manager.get_area_by_id(area_id) for c in target_area.clients: c.send_ic(params=pargs, sender=self.client, gag_replaced=gag_replaced) target_area.set_next_msg_delay(len(msg)) # Deal with shoutlog if pargs['button'] > 0: info = 'used shout {} with the message: {}'.format( pargs['button'], msg) target_area.add_to_shoutlog(self.client, info) self.client.area.set_next_msg_delay(len(msg)) logger.log_server( '[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client) # Sending IC messages reveals sneaked players if not self.client.is_staff() and not self.client.is_visible: self.client.change_visibility(True) self.client.send_ooc_others( '(X) {} [{}] revealed themselves by talking ({}).'.format( self.client.displayname, self.client.id, self.client.area.id), is_zstaff=True) self.server.tasker.create_task(self.client, [ 'as_afk_kick', self.client.area.afk_delay, self.client.area.afk_sendto ]) if self.client.area.is_recording: self.client.area.recorded_messages.append(args) self.client.last_ic_message = msg self.client.last_active = Constants.get_time()
def dump_hdids(self): with Constants.fopen('storage/hd_ids.json', 'w') as whole_list: json.dump(self.hdid_list, whole_list)
def load_characters(self): with Constants.fopen('config/characters.yaml', 'r', encoding='utf-8') as chars: self.char_list = Constants.yaml_load(chars) self.build_char_pages_ao1()
def receive_command_stc(self, command_type, *args): command_type, *args = Constants.decode_ao_packet([command_type] + list(args)) self.received_packets.append([command_type, tuple(args)]) buffer = '' if command_type == 'decryptor': # Hi buffer = 'HI#FAKEHDID#%' elif command_type == 'ID': # Server ID buffer = "ID#DRO#1.0.0#%" err = ('Wrong client ID for {}.\nExpected {}\nGot {}' .format(self, args[0], self.id)) assert args[0] == str(self.id), err elif command_type == 'FL': # AO 2.2.5 configs pass elif command_type == 'PN': # Player count pass elif command_type == 'SI': # Counts for char/evi/music pass elif command_type == 'SC': # Character list # TODO: RC!!! pass elif command_type == 'SM': # First timer music/area list pass elif command_type == 'CharsCheck': # Available characters pass elif command_type == 'HP': # Def/pro bar pass elif command_type == 'BN': # Background file pass elif command_type == 'LE': # Evidence list pass elif command_type == 'MM': # ????????????? pass elif command_type == 'OPPASS': # Guard pass pass elif command_type == 'DONE': # Done setting up courtroom pass elif command_type == 'CT': # OOC message self.received_ooc.append((args[0], args[1])) elif command_type == 'FM': # Updated music/area list pass elif command_type == 'PV': # Current character pass elif command_type == 'MS': # IC message # 0 = msg_type # 1 = pre # 2 = folder # 3 = anim # 4 = msg # 5 = pos # 6 = sfx # 7 = anim_type # 8 = char_id # 9 = sfx_delay # 10 = button # 11 = self.client.evi_list[evidence] # 12 = flip # 13 = ding # 14 = color # 15 = showname if not (len(args) == 16): raise ValueError('Malformed MS packet for an IC message {}'.format(args)) self.received_ic.append(args) elif command_type == 'MC': # Start playing track pass elif command_type == 'ZZ': # Mod call pass elif command_type == 'GM': # Gamemode switch pass elif command_type == 'TOD': # Time of day switch pass elif command_type == 'ackMS': # Acknowledge MS packet pass elif command_type == 'SN': # Showname change pass else: raise KeyError('Unrecognized STC argument `{}` {}'.format(command_type, args)) if buffer: self.send_command_cts(buffer)