From 2f9e33b357f2c40a326f292c282e67858a007f1e Mon Sep 17 00:00:00 2001 From: Chrezm <42015761+Chrezm@users.noreply.github.com> Date: Sun, 30 May 2021 14:02:06 -0400 Subject: [PATCH] Chrini packet (#120) * Basic processing * Basic functionality * Add /charlog * Add character showname checking logic * Add character details to client info * Bump up version * Make commands support char showname+Standardize /whois identifier type lookup priority --- CHANGELOG.md | 6 ++- README.md | 20 ++++--- server/aoprotocol.py | 22 +++++++- server/area_manager.py | 4 +- server/client_changearea.py | 16 +++++- server/client_manager.py | 93 +++++++++++++++++++++++++-------- server/clients.py | 5 ++ server/commands.py | 40 ++++++++++++-- server/tsuserver.py | 6 +-- tests/test_client_connection.py | 2 +- 10 files changed, 171 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 594f2987..2fbda1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -490,7 +490,7 @@ ### 210213b (4.2.5-post8) * Fixed regression where if ip_ids.json or hd_ids.json did not exist, the server would not launch -### (4.3.0) +## (4.3.0) * Tied in to Danganronpa Online v1.0.0, although support for the previous Danganronpa Online version will be kept for 4.3.0 * Explicitly allowed Python 3.9 support for server owners * Added basic DR-style trials (confront readme for command instructions): @@ -598,6 +598,9 @@ * If an area is made part of a zone via /zone or /zone_add, all players are now notified about it. A similar behavior occurs now with /zone_remove * All /showname_set notifications now include the old showname of the affected user if applicable * Clients may now send empty sound effects +* Made /showname_history be available to all staff members (previously it was for moderators only) +* Added /charlog, which lists all character changes a player has gone through in a session (including character showname or iniswap changes) +* Made /whois identifiers follow the same identifier type lookup logic as other commands * Fixed scream_range in area list yaml files not supporting the keyword to indicate all areas should be able to receive a scream coming from a particular area * Fixed scream_range in area list yaml files not checking if the areas a scream can reach to from a particular area exist * Fixed /scream, /whisper and /party_whisper raising errors if a message was sent to a deafened player with a bypass message starter. They now sent messages but filtered @@ -632,5 +635,6 @@ * Fixed /play bypassing IC mutes, blockdj and the server music flood guard * Fixed /showname_set stopping early if multiple targets needed to be updated but an early one failed * Fixed the default config.yaml listing 'announce_areas' as an unused parameter (it is actively used) +* Fixed /showname_set being listed as a moderator only command in the README (it was always staff only) * Removed deprecated AO commands, and deprecated packets opKICK and opBAN * Dropped Python 3.6 support diff --git a/README.md b/README.md index afe6d8db..784d1165 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,8 @@ GMs can: * **char_restrict** "character name" - Changes the restricted status of a character in the current area. - If a character is restricted, only GMs and above can use the character in the current area. +* **charlog** "ID" + - Lists all character changes (including iniswaps and character name changes) a target has gone through since connecting, including the time they were changed. * **clock** "area range start" "area range end" "hour length" "hour start" - Sets up a day cycle that, starting from the given hour, will tick one hour every given number of seconds and provide a time announcement to a given range of areas. * **clock_cancel** "ID" @@ -381,7 +383,7 @@ GMs can: * **iclock** - Changes the IC lock status of the current area. - If the area has an IC lock, only GMs and above will be able to send IC messages. -* **iclock_bypass* "ID" +* **iclock_bypass** "ID" - Grants/revokes of an IC lock bypass to the target. - Targets with an IC lock bypass may talk in an area whose IC chat is locked. This effect disappears automatically as soon as they move area or their IC chat is unlocked. * **judgelog** "area" @@ -465,8 +467,10 @@ GMs can: * **shoutlog** "area" - Lists the last 20 shouts sent in the given area, or from the current area if not given. - Each entry includes the time of execution, client ID, character name, client IPID, the shout ID and the IC message sent alongside. -* **showname_area** "area" - - Similar to /getarea, but lists shownames along with character names. +* **showname_history** "ID" + - Lists all shownames a target has gone through since connecting, including the time they were changed. +* **showname_set** "ID" "showname" + - Sets a target's showname to be the given one, or clears it if not given one. * **sneak** "ID" - Sets a target to be sneaking if they were visible. - If the target was subject to a handicap shorter than the server's automatic sneak handicap length, they will be imposed this handicap. @@ -570,6 +574,8 @@ GMs can: - If not given a target area, it will use the server's default area (usually area 0). * **blockdj** "ID/IPID" - Mutes the target from changing music. +* **charlog** "ID/IPID" + - Lists all character changes (including iniswaps and character name changes) a target has gone through since connecting, including the time they were changed. * **cleargm** "ID" - Logs out the target from their GM rank, or all GMs in the server if not given a target, and puts them in RP mode if needed. * **g** "message" @@ -603,6 +609,10 @@ GMs can: - Similar to /getarea, but lists shownames along with character names as well as their IPIDs. * **showname_areas** - Similar to /getareas, but lists shownames along with character names as well as their IPIDs. +* **showname_history** "ID/IPID" + - Lists all shownames a target has gone through since connecting, including the time they were changed. +* **showname_set** "ID/IPID" "showname" + - Sets a target's showname to be the given one, or clears it if not given one. * **sneak** "ID/IPID" - Sets a target to be sneaking if they were visible. - If the target was subject to a handicap shorter than the server's automatic sneak handicap length, they will be imposed this handicap. @@ -659,12 +669,8 @@ GMs can: - Reloads the server's default character, music and background lists. * **showname_freeze** - Changes the ability of non-staff members of being able to change or remove their own shownames. -* **showname_history** "ID/IPID" - - Lists all shownames a target has gone through since connecting, including the time they were changed. * **showname_nuke** - Clears all shownames from non-staff members. -* **showname_set** "ID/IPID" "showname" - - Sets a target's showname to be the given one, or clears it if not given one.* **switch** "character name" * **switch** "character name" - Switches you to the given character. If some other player in the area is using it, they will be forced to the character select screen. * **unban** "IPID/IP" diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 8799a756..7609a270 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -343,7 +343,7 @@ def check_client_version(): 'noencryption', 'deskmod', 'evidence', 'cccc_ic_support', 'looping_sfx', 'additive', 'effects', # DRO exclusive stuff - 'ackMS', 'showname'] + 'ackMS', 'showname', 'chrini'] }) def net_cmd_ch(self, args: List[str]): @@ -664,7 +664,8 @@ def net_cmd_ms(self, args: List[str]): 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 not self.client.char_folder: + self.client.char_folder = pargs['folder'] if pargs['anim_type'] not in (5, 6): self.client.last_sprite = pargs['anim'] @@ -1023,11 +1024,27 @@ def net_cmd_sn(self, args: List[str]): pargs = self.process_arguments('SN', args) self.client.publish_inbound_command('SN', pargs) + if self.client.showname == pargs['showname']: + return + try: self.client.command_change_showname(pargs['showname'], False) except ClientError as exc: self.client.send_ooc(exc) + def net_cmd_chrini(self, args: List[str]): + """ + Char.ini information + """ + + pargs = self.process_arguments('chrini', args) + self.client.publish_inbound_command('chrini', pargs) + + self.client.change_character_ini_details( + pargs['actual_folder_name'], + pargs['actual_character_showname'], + ) + def net_cmd_re(self, _): # Ignore packet return @@ -1071,6 +1088,7 @@ def net_cmd_opBAN(self, _): 'PW': net_cmd_pw, # character password (only on CC/KFO clients), deprecated 'SP': net_cmd_sp, # set position 'SN': net_cmd_sn, # set showname + 'chrini': net_cmd_chrini, # char.ini information 'opKICK': net_cmd_opKICK, # /kick with guard on, deprecated 'opBAN': net_cmd_opBAN, # /ban with guard on, deprecated } diff --git a/server/area_manager.py b/server/area_manager.py index b4e0b5b6..d7baedd6 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -25,7 +25,7 @@ from __future__ import annotations import typing -from typing import Callable, Dict, List, Set, Tuple +from typing import Any, Callable, Dict, List, Set, Tuple if typing.TYPE_CHECKING: # Avoid circular referencing from server.client_manager import ClientManager @@ -56,7 +56,7 @@ class Area: Create a new area for the server. """ - def __init__(self, area_id: int, server: TsuserverDR, parameters: Dict[Str, Any]): + def __init__(self, area_id: int, server: TsuserverDR, parameters: Dict[str, Any]): """ Parameters ---------- diff --git a/server/client_changearea.py b/server/client_changearea.py index 7705e2b4..010cb6a3 100644 --- a/server/client_changearea.py +++ b/server/client_changearea.py @@ -178,7 +178,7 @@ def notify_me(self, area: AreaManager.Area, old_dname: str, # Check if someone in the new area has the same showname try: # Verify that showname is still valid - client.change_showname(client.showname, target_area=area) + client.check_change_showname(client.showname, target_area=area) except ValueError: client.send_ooc('Your showname `{}` was already used in this area, so it has been ' 'removed.'.format(client.showname)) @@ -189,6 +189,20 @@ def notify_me(self, area: AreaManager.Area, old_dname: str, client.change_showname('', target_area=area) logger.log_server('{} had their showname removed due it being used in the new area.' .format(client.ipid), client) + + # Check if someone in the new area has the same character showname + try: # Verify that the character showname is still valid + client.check_change_showname(client.char_showname, target_area=area) + except ValueError: + client.send_ooc('Your character showname `{}` was already used in this area, so it has ' + 'been removed.'.format(client.char_showname)) + client.send_ooc_others('(X) Client {} had their character showname `{}` removed in ' + 'your zone due to it conflicting with the showname of another ' + 'player in the same area ({}).' + .format(client.id, client.showname, area.id), is_zstaff=area) + client.change_character_ini_details(client.char_folder, '') + logger.log_server('{} had their character showname removed due it being used in the ' + 'new area.'.format(client.ipid), client) ########### # Check if the lights were turned off, and if so, let you know, if you are not blind diff --git a/server/client_manager.py b/server/client_manager.py index 3b81d56c..7aa0c9f3 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -18,7 +18,7 @@ from __future__ import annotations import typing -from typing import Callable, List, Optional, Set, Tuple +from typing import Any, Callable, List, Optional, Set, Tuple if typing.TYPE_CHECKING: # Avoid circular referencing from server.area_manager import AreaManager @@ -58,6 +58,7 @@ def __init__(self, server: TsuserverDR, transport, user_id: int, ipid: int, self.char_id = None self.name = '' self.char_folder = '' + self.char_showname = '' self.pos = '' self.showname = '' self.ever_chose_character = False @@ -119,6 +120,7 @@ def __init__(self, server: TsuserverDR, transport, user_id: int, ipid: int, self.remembered_locked_passages = dict() self.remembered_statuses = dict() self.can_bypass_iclock = False + self.char_log = list() # Pairing stuff self.charid_pair = -1 @@ -663,6 +665,8 @@ def is_valid_name(self, name: str) -> bool: def displayname(self) -> str: if self.showname: return self.showname + if self.char_showname: + return self.char_showname return self.get_char_name() def change_character(self, char_id: int, force: bool = False, @@ -721,7 +725,8 @@ def change_character(self, char_id: int, force: bool = False, self.check_lurk() self.char_id = char_id - self.char_folder = self.get_char_name() # Assumes players are not iniswapped initially + self.char_folder = self.get_char_name() # Assumes players are not iniswapped initially, waiting for chrini packet + self.char_showname = '' # Assumes players are not iniswapped initially, waiting for chrini packet self.pos = '' if announce_zwatch: self.send_ooc_others('(X) Client {} has changed from character `{}` to `{}` in ' @@ -740,7 +745,8 @@ def change_character(self, char_id: int, force: bool = False, }) logger.log_server('[{}]Changed character from {} to {}.' .format(self.area.id, old_char, self.get_char_name()), self) - + self.add_to_charlog(f'Changed character to {self.get_char_name()}.') + def change_music_cd(self) -> int: if self.is_staff(): return 0 @@ -893,13 +899,13 @@ def change_gagged(self, gagged: bool ): # changed = (self.is_gagged != gagged) self.is_gagged = gagged - def change_showname(self, showname: str, target_area: AreaManager.Area = None, - forced: bool = True): - # forced=True means that someone else other than the user themselves requested the - # showname change. Should only be false when using /showname. + def check_change_showname(self, showname: str, target_area: AreaManager.Area = None): + if not showname: + # Empty shownames are always fine + return if target_area is None: target_area = self.area - + if Constants.contains_illegal_characters(showname): raise ClientError(f'Showname `{showname}` contains an illegal character.') @@ -908,14 +914,40 @@ def change_showname(self, showname: str, target_area: AreaManager.Area = None, raise ClientError("Showname `{}` exceeds the server's character limit of {}." .format(showname, self.server.config['showname_max_length'])) - # Check if non-empty showname is already used within area - if showname != '': - for c in target_area.clients: - if c.showname == showname and c != self: - raise ValueError("Showname `{}` is already in use in this area." - .format(showname)) - # This ValueError must be recaught, otherwise the client will crash. + # Check if showname is already used within area + for c in target_area.clients: + if c == self: + continue + if c.showname == showname or c.char_showname == showname: + raise ValueError("Showname `{}` is already in use in this area." + .format(showname)) + # This ValueError must be recaught, otherwise the client will crash. + + def change_character_ini_details(self, char_folder: str, char_showname: str): + self.char_folder = char_folder + + # Check if new character showname is valid before updating. + try: + if char_showname and self.server.showname_freeze and not self.is_staff(): + raise ClientError('Shownames are frozen.') + self.check_change_showname(char_showname, target_area=self.area) + except (ClientError, ValueError) as exc: + self.send_ooc(f'Unable to update character showname: {exc}') + else: + self.char_showname = char_showname + + self.add_to_charlog( + f'Changed character ini to {self.char_folder}/{self.char_showname}.') + + def change_showname(self, showname: str, target_area: AreaManager.Area = None, + forced: bool = True): + # forced=True means that someone else other than the user themselves requested the + # showname change. Should only be false when using /showname. + if target_area is None: + target_area = self.area + self.check_change_showname(showname, target_area=target_area) + if self.showname != showname: status = {True: 'Was', False: 'Self'} ctime = Constants.get_time() @@ -1667,9 +1699,9 @@ def logout(self): .format(self.displayname, self.id), part_of=target_zone.get_watchers()) elif target_zone.get_players(): - client.send_ooc('(X) Warning: The zone no longer has any watchers.') + self.send_ooc('(X) Warning: The zone no longer has any watchers.') else: - client.send_ooc('(X) As you were the last person in an area part of it or who ' + self.send_ooc('(X) As you were the last person in an area part of it or who ' 'was watching it, your zone has been deleted.') # Not needed, ran in remove_watcher # client.send_ooc_others('Zone `{}` was automatically deleted as no one was in ' @@ -1755,7 +1787,25 @@ def get_multiclients(self) -> List[ClientManager.Client]: ipid = self.server.client_manager.get_targets(self, TargetType.IPID, self.ipid, False) hdid = self.server.client_manager.get_targets(self, TargetType.HDID, self.hdid, False) return sorted(set(ipid + hdid)) - + + def add_to_charlog(self, text: str): + ctime = Constants.get_time() + if len(self.char_log) >= 20: + self.char_log.pop(0) + + self.char_log.append(f'{ctime} | {text}') + + def get_charlog(self) -> str: + info = '== Character details log of client {} =='.format(self.id) + + if not self.char_log: + info += ('\r\nClient has not changed their character information since joining the ' + 'server.') + else: + for log in self.char_log: + info += '\r\n*{}'.format(log) + return info + def get_info(self, as_mod: bool = False, as_cm: bool = False, identifier=None): if identifier is None: identifier = self.id @@ -1764,11 +1814,10 @@ def get_info(self, as_mod: bool = False, as_cm: bool = False, identifier=None): ipid = self.ipid if as_mod or as_cm else "-" hdid = self.hdid if as_mod or as_cm else "-" info += '\n*CID: {}. IPID: {}. HDID: {}'.format(self.id, ipid, hdid) - char_info = self.get_char_name() - if self.char_folder and self.char_folder != char_info: # Indicate iniswap if needed - char_info = '{} ({})'.format(char_info, self.char_folder) info += ('\n*Character name: {}. Showname: {}. OOC username: {}' - .format(char_info, self.showname, self.name)) + .format(self.get_char_name(), self.showname, self.name)) + info += ('\n*Actual character folder: {}. Character showname: {}.' + .format(self.char_folder, self.char_showname)) info += '\n*In area: {}-{}'.format(self.area.id, self.area.name) info += '\n*Last IC message: {}'.format(self.last_ic_message) info += '\n*Last OOC message: {}'.format(self.last_ooc_message) diff --git a/server/clients.py b/server/clients.py index a9e1924a..1d6220ef 100644 --- a/server/clients.py +++ b/server/clients.py @@ -280,6 +280,11 @@ class DefaultAO2Protocol(Enum): SN_OUTBOUND = [ ('showname', ''), # 0 ] + + CHRINI_INBOUND = [ + ('actual_folder_name', ArgType.STR), # 0 + ('actual_character_showname', ArgType.STR), # 1 + ] ClientDRO1d0d0 = Enum('ClientDRO1d0d0', [(m.name, m.value) for m in DefaultAO2Protocol]) diff --git a/server/commands.py b/server/commands.py index cc882d66..c673d836 100644 --- a/server/commands.py +++ b/server/commands.py @@ -4485,8 +4485,8 @@ def ooc_cmd_rollp(client: ClientManager.Client, arg: str): client.add_to_dicelog(roll_message + '.') client.area.add_to_dicelog(client, roll_message + '.') - SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) - encoding = hashlib.sha1((str(roll_result) + SALT).encode('utf-8')).hexdigest() + '|' + SALT + salt = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) + encoding = hashlib.sha1((str(roll_result) + salt).encode('utf-8')).hexdigest() + '|' + salt logger.log_server('[{}][{}]Used /rollp and got {} out of {}.' .format(client.area.id, client.get_char_name(), encoding, num_faces), client) @@ -5006,7 +5006,7 @@ def ooc_cmd_showname_freeze(client: ClientManager.Client, arg: str): def ooc_cmd_showname_history(client: ClientManager.Client, arg: str): - """ (MOD ONLY) + """ (STAFF ONLY) List all shownames a client by ID or IPID has had during the session. Output differentiates between self-initiated showname changes (such as the ones via /showname) by using "Self" and third-party-initiated ones by using "Was" (such as /showname_set, or by changing areas and @@ -5036,7 +5036,7 @@ def ooc_cmd_showname_history(client: ClientManager.Client, arg: str): *Sat Jun 1 18:54:46 2019 | Was cleared """ - Constants.assert_command(client, arg, is_mod=True, parameters='=1') + Constants.assert_command(client, arg, is_staff=True, parameters='=1') # Obtain matching targets's showname history for c in Constants.parse_id_or_ipid(client, arg): @@ -9358,6 +9358,38 @@ def ooc_cmd_zone_unhandicap(client: ClientManager.Client, arg: str): continue +def ooc_cmd_charlog(client: ClientManager.Client, arg: str): + """ (STAFF ONLY) + List all character details a client by ID or IPID has had during the session. + + If given IPID, it will obtain the character details log of all the clients opened by the user. + Otherwise, it will just obtain the log of the given client. + Returns an error if the given identifier does not correspond to a user. + + SYNTAX + /charlog + /charlog + + PARAMETERS + : Client identifier (number in brackets in /getarea) + : IPID for the client (number in parentheses in /getarea) + + EXAMPLE + /charlog 1 :: For the client whose ID is 1, you may get something like this + + == Character details log of client 1 == + *Sat Jun 1 18:52:32 2021 | Changed character to Phantom_HD + *Sat Jun 1 18:52:32 2021 | Changed character ini to Phantom_HD/Phantom + """ + + Constants.assert_command(client, arg, is_staff=True, parameters='=1') + + # Obtain matching targets's character details log + for c in Constants.parse_id_or_ipid(client, arg): + info = c.get_charlog() + client.send_ooc(info) + + def ooc_cmd_exec(client: ClientManager.Client, arg: str): """ VERY DANGEROUS. SHOULD ONLY BE ENABLED FOR DEBUGGING. diff --git a/server/tsuserver.py b/server/tsuserver.py index 620b9890..606dc3c0 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -20,7 +20,7 @@ # This class will suffer major reworkings for 4.3 from __future__ import annotations -from typing import Any, Dict, List, Tuple +from typing import Any, Callable, Dict, List, Tuple import asyncio import errno @@ -68,8 +68,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 0 - self.segment_version = 'b164' - self.internal_version = 'M210511a' + self.segment_version = 'b165' + self.internal_version = 'M210530a' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) diff --git a/tests/test_client_connection.py b/tests/test_client_connection.py index 967054f3..0761fed6 100644 --- a/tests/test_client_connection.py +++ b/tests/test_client_connection.py @@ -2,7 +2,7 @@ _standard_FL = ('yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'cccc_ic_support', 'looping_sfx', 'additive', 'effects', - 'ackMS', 'showname') + 'ackMS', 'showname', 'chrini') class TestClientConnection(_Unittest):