From fadcd3ea66c7c874e33ba9dc7e2ab4a3760590fb Mon Sep 17 00:00:00 2001 From: Chrezm Date: Fri, 12 Aug 2022 18:05:52 -0400 Subject: [PATCH 01/13] Fix area validator not catching areas with empty name --- server/tsuserver.py | 6 +++--- server/validate/areas.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/tsuserver.py b/server/tsuserver.py index aad441de3..b74f7309c 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -68,9 +68,9 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 - self.minor_version = 3 - self.segment_version = 'post2' - self.internal_version = '220723a' + self.minor_version = 4 + self.segment_version = 'a1' + self.internal_version = '220812a' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) diff --git a/server/validate/areas.py b/server/validate/areas.py index da6fa26db..c9d0a1d60 100644 --- a/server/validate/areas.py +++ b/server/validate/areas.py @@ -69,10 +69,10 @@ def validate_contents(self, contents, extra_parameters=None): # Create the areas for item in contents: # Check required parameters - if 'area' not in item: + if 'area' not in item or not item['area']: info = 'Area {} has no name.'.format(current_area_id) raise AreaError(info) - if 'background' not in item: + if 'background' not in item or not item['background']: info = 'Area {} has no background.'.format(item['area']) raise AreaError(info) From 66bf81b75035ab88ba11c1e08639f78a0a9d609b Mon Sep 17 00:00:00 2001 From: Chrezm Date: Fri, 12 Aug 2022 19:30:26 -0400 Subject: [PATCH 02/13] Added last_received_sprites to API of send_ic_others --- server/client_manager.py | 2 ++ server/tsuserver.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/client_manager.py b/server/client_manager.py index 9b18d9a26..bb4bb6dd2 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -519,6 +519,7 @@ def pop_if_there(dictionary, argument): def send_ic_others(self, params: List = None, sender: ClientManager.Client=None, bypass_text_replace: bool = False, bypass_deafened_starters: bool = False, + use_last_received_sprites: bool = False, pred: Callable[[ClientManager.Client], bool] = None, not_to=None, gag_replaced=False, is_staff=None, in_area=None, to_blind=None, to_deaf=None, msg=None, folder=None, pos=None, char_id=None, ding=None, @@ -532,6 +533,7 @@ def send_ic_others(self, params: List = None, sender: ClientManager.Client=None, for c in self.server.get_clients(): c.send_ic(params=None, sender=sender, bypass_text_replace=bypass_text_replace, bypass_deafened_starters=bypass_deafened_starters, + use_last_received_sprites=use_last_received_sprites, pred=pred, not_to=not_to, gag_replaced=gag_replaced, is_staff=is_staff, in_area=in_area, to_blind=to_blind, to_deaf=to_deaf, msg=msg, folder=folder, pos=pos, char_id=char_id, ding=ding, color=color, diff --git a/server/tsuserver.py b/server/tsuserver.py index b74f7309c..5d98c9026 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a1' - self.internal_version = '220812a' + self.segment_version = 'a2' + self.internal_version = '220812b' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) From 2fa8890d370ba02e89523a60880a43e739abbfa7 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Fri, 12 Aug 2022 19:34:07 -0400 Subject: [PATCH 03/13] Fix typo in field of area_templates --- CHANGELOG.md | 4 ++++ config_sample/area_templates.yaml | 6 +++--- server/tsuserver.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e677fad42..9c8afa5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -779,3 +779,7 @@ ### 220723a (4.3.3-post2) * Fixed /zone_ambient not allowing tracks with spaces + +### (4.3.4) +* Fixed area validator not properly handling areas with empty names or backgrounds +* Fixed typo in area_templates.yaml field diff --git a/config_sample/area_templates.yaml b/config_sample/area_templates.yaml index ac8719af9..2b54fb2d0 100644 --- a/config_sample/area_templates.yaml +++ b/config_sample/area_templates.yaml @@ -17,7 +17,7 @@ locking_allowed: true iniswap_allowed: true rp_getarea_allowed: false -- template_name: AO_Courtroom +- template_name: AO_Courtroom area: Courtroom No. background: gs4 bglock: true @@ -28,5 +28,5 @@ - template_name: OOC_Stuff area: OOC background: Principal's Room_HD - eivdence_mod: HiddenCM - iniswap_allowed: true \ No newline at end of file + evidence_mod: HiddenCM + iniswap_allowed: true diff --git a/server/tsuserver.py b/server/tsuserver.py index 5d98c9026..3365d7c76 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a2' - self.internal_version = '220812b' + self.segment_version = 'a3' + self.internal_version = '220812c' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) From a38002b1fc96d39c3d44626202ba64d6d952064d Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 13 Aug 2022 17:44:43 -0400 Subject: [PATCH 04/13] Add /sneakself --- CHANGELOG.md | 1 + README.md | 2 ++ server/commands.py | 44 +++++++++++++++++++++++++++++++++++++++++++- server/tsuserver.py | 4 ++-- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c8afa5ed..b72c32c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -781,5 +781,6 @@ * Fixed /zone_ambient not allowing tracks with spaces ### (4.3.4) +* Added /sneakself, which sneaks all of your active multiclients that are not currently sneaked but can be sneaked * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field diff --git a/README.md b/README.md index 92b7c64d9..fb037c963 100644 --- a/README.md +++ b/README.md @@ -541,6 +541,8 @@ GMs can: - 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. - If no ID is given, target is yourself. +* **sneakself** + - Sneaks all opened multiclients that can be sneaked. * **st** "message" - Sends a message to all active staff members. * **status_set_other** "ID/char name/edited-to character/showname/char showname/OOC name" "status" diff --git a/server/commands.py b/server/commands.py index b4189cffd..5c242d0e9 100644 --- a/server/commands.py +++ b/server/commands.py @@ -2894,8 +2894,9 @@ def ooc_cmd_gmlock(client: ClientManager.Client, arg: str): def ooc_cmd_gmself(client: ClientManager.Client, arg: str): - """ (STAFF ONLY): + """ (STAFF ONLY) Makes all opened multiclients login as game master without them needing to put in a GM password. + Opened multiclients that are already logged in as game master are unaffected. Returns an error if all opened multiclients are already game masters. SYNTAX @@ -11226,6 +11227,47 @@ def ooc_cmd_zone_ambient_end(client: ClientManager.Client, arg: str): a.ambient = '' +def ooc_cmd_sneakself(client: ClientManager.Client, arg: str): + """ (STAFF ONLY+VARYING REQUIREMENTS) + Makes all opened multiclients be sneaked without having to manually sneak them. + Opened multiclients that are already sneaked are unaffected. + If a multiclient is in a private area, or in a lobby area and you are not an officer, or is + already sneaked, the sneak will fail for that multiclient. + Returns an error if no opened multiclients can successfully be sneaked. + + SYNTAX + /sneakself + + EXAMPLES + If user with client ID 0 is GM has multiclients with ID 1 and 3, neither sneaked, and runs... + >>> /sneakself + Sneaks clients 0, 1 and 3. + """ + + Constants.assert_command(client, arg, is_staff=True) + + targets = [c for c in client.get_multiclients() if c.is_visible] + targets = [c for c in targets if not c.area.private_area] + if not client.is_officer(): + targets = [c for c in targets if c.area.lobby_area] + if not targets: + raise ClientError('No opened clients can be sneaked.') + + # Sneak matching targets + for c in targets: + c.change_visibility(False) + + client.send_ooc("You sneaked all of your valid multiclients.") + client.send_ooc_others(f'(X) {client.displayname} [{client.id}] sneaked all their valid ' + f'multiclients [{client.id}] ({client.area.id}).', + not_to=set(targets), is_zstaff=True) + + non_targets = [c for c in client.get_multiclients() if c not in targets] + if non_targets: + s_non_targets = Constants.cjoin([f'{c.displayname} [{c.id}]' for c in non_targets]) + client.send_ooc(f'The following clients could not be sneaked: {s_non_targets}') + + 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 3365d7c76..afc902216 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a3' - self.internal_version = '220812c' + self.segment_version = 'a4' + self.internal_version = 'm220813a' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) From 68a7f180825c63e7a8efadd787fdc20f9edd0096 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 13 Aug 2022 18:29:09 -0400 Subject: [PATCH 05/13] GMs are no longer subject to the duplicate showname checks --- CHANGELOG.md | 1 + server/client_manager.py | 17 +++++++++-------- server/tsuserver.py | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b72c32c28..9353a971f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -782,5 +782,6 @@ ### (4.3.4) * Added /sneakself, which sneaks all of your active multiclients that are not currently sneaked but can be sneaked +* GMs are no longer subject to the duplicate showname checks * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field diff --git a/server/client_manager.py b/server/client_manager.py index bb4bb6dd2..49a82d17a 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -987,14 +987,15 @@ def check_change_showname(self, showname: str, target_area: AreaManager.Area = N raise ClientError("Showname `{}` exceeds the server's character limit of {}." .format(showname, self.server.config['showname_max_length'])) - # 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. + # Check if showname is already used within area, and not GM + if not self.is_staff(): + 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 diff --git a/server/tsuserver.py b/server/tsuserver.py index afc902216..d3eb082fa 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a4' - self.internal_version = 'm220813a' + self.segment_version = 'a5' + self.internal_version = 'm220813b' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) From 3c34deba82f81bfa20a2178ae8678867a72a0901 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Fri, 19 Aug 2022 22:40:29 -0400 Subject: [PATCH 06/13] Add /mindreader, fix thoughts not showing last sprite, show thoughts in IC if same area --- CHANGELOG.md | 3 ++ README.md | 2 + server/client_manager.py | 98 +++++++++++++++++++++++++++++++--------- server/commands.py | 72 +++++++++++++++++++++++++++-- server/tsuserver.py | 22 +++++---- 5 files changed, 161 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9353a971f..c6c2e737e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -782,6 +782,9 @@ ### (4.3.4) * Added /sneakself, which sneaks all of your active multiclients that are not currently sneaked but can be sneaked +* GMs and zone watchers now see thoughts in IC if the thinker is in the same area +* Added /mindreader, which allows GMs to let mind readers to see players using /think just like GMs now do * GMs are no longer subject to the duplicate showname checks * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field +* Fixed /think messages not showing the last sent sprite diff --git a/README.md b/README.md index fb037c963..64339f1e6 100644 --- a/README.md +++ b/README.md @@ -446,6 +446,8 @@ GMs can: - Ends the area's lurk callout timer if there is one active. * **make_gm** "ID" - Makes the target a GM, provided the target is a multiclient of the player. +* **mindreader** "ID" + - Changes a player's ability to see thoughts of other players made with /think. By default it is off. * **multiclients** "ID" - Lists all the clients opened by a target and the areas they are in. * **notecard_check** diff --git a/server/client_manager.py b/server/client_manager.py index 49a82d17a..3353ea0e3 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -128,6 +128,7 @@ def __init__(self, server: TsuserverDR, transport, user_id: int, ipid: int, self.ignored_players = set() self.paranoia = 2 self.notecard = '' + self.is_mindreader = False # Pairing stuff self.charid_pair = -1 @@ -252,14 +253,31 @@ def send_ooc_others(self, msg: str, username: str = None, allow_empty: bool = Fa self.server.make_all_clients_do("send_ooc", msg, pred=cond, allow_empty=allow_empty, username=username) - def send_ic(self, params: List = None, sender: ClientManager.Client = None, + def send_ic(self, + params: List = None, + sender: ClientManager.Client = None, + bypass_text_replace: bool = False, + bypass_deafened_starters: bool = False, + use_last_received_sprites: bool = False, + gag_replaced: bool = False, pred: Callable[[ClientManager.Client], bool] = None, - not_to: ClientManager.Client = None, gag_replaced=False, - is_staff=None, in_area=None, to_blind=None, to_deaf=None, - bypass_text_replace=False, bypass_deafened_starters=False, - use_last_received_sprites=False, - msg=None, folder=None, pos=None, char_id=None, ding=None, color=None, - showname=None, hide_character=0): + not_to: Set[ClientManager.Client] = None, + part_of: Set[ClientManager.Client] = None, + is_staff: bool = None, + is_officer: bool = None, + is_zstaff: bool = None, + is_zstaff_flex: bool = None, + in_area: bool = None, + to_blind: bool = None, + to_deaf: bool = None, + msg=None, + folder=None, + pos=None, + char_id=None, + ding=None, + color=None, + showname=None, + hide_character=0): # sender is the client who sent the IC message # self is who is receiving the IC message at this particular moment @@ -268,6 +286,9 @@ def send_ic(self, params: List = None, sender: ClientManager.Client = None, if params is None and msg is None: raise ValueError('Expected message.') + if not_to is None: + not_to = set() + # Fill in defaults # Expected behavior is as follows: # If params is None, then the sent IC message will only include custom details @@ -295,8 +316,11 @@ def send_ic(self, params: List = None, sender: ClientManager.Client = None, # Check if receiver is actually meant to receive the message. Bail out early if not. # FIXME: First argument should be sender, not self. Using in_area=True fails otherwise - cond = Constants.build_cond(self, is_staff=is_staff, in_area=in_area, not_to=not_to, - to_blind=to_blind, to_deaf=to_deaf, pred=pred) + cond = Constants.build_cond(self, is_staff=is_staff, is_officer=is_officer, + in_area=in_area, not_to=not_to, + part_of=part_of, to_blind=to_blind, to_deaf=to_deaf, + is_zstaff=is_zstaff, is_zstaff_flex=is_zstaff_flex, + pred=pred) if not cond(self): return # If self is ignoring sender, now is the moment to discard @@ -400,6 +424,7 @@ def pop_if_there(dictionary, argument): pargs['pos'] = last_args['pos'] pargs['anim_type'] = last_args['anim_type'] pargs['flip'] = last_args['flip'] + pargs['hide_character'] = last_args['hide_character'] # Regardless of anything, pairing is visually canceled while in first person # so set them to default values @@ -516,28 +541,57 @@ def pop_if_there(dictionary, argument): self.send_command_dict('MS', final_pargs) - def send_ic_others(self, params: List = None, sender: ClientManager.Client=None, + def send_ic_others(self, + params: List = None, + sender: ClientManager.Client = None, bypass_text_replace: bool = False, bypass_deafened_starters: bool = False, use_last_received_sprites: bool = False, - pred: Callable[[ClientManager.Client], bool] = None, not_to=None, - gag_replaced=False, is_staff=None, in_area=None, to_blind=None, - to_deaf=None, msg=None, folder=None, pos=None, char_id=None, ding=None, - color=None, showname=None, hide_character=0): + gag_replaced: bool = False, + pred: Callable[[ClientManager.Client], bool] = None, + not_to: Set[ClientManager.Client] = None, + part_of: Set[ClientManager.Client] = None, + is_staff: bool = None, + is_officer: bool = None, + is_zstaff: bool = None, + is_zstaff_flex: bool = None, + in_area: bool = None, + to_blind: bool = None, + to_deaf: bool = None, + msg=None, + folder=None, + pos=None, + char_id=None, + ding=None, + color=None, + showname=None, + hide_character=0): if not_to is None: not_to = {self} else: not_to = not_to.union({self}) - for c in self.server.get_clients(): - c.send_ic(params=None, sender=sender, bypass_text_replace=bypass_text_replace, - bypass_deafened_starters=bypass_deafened_starters, - use_last_received_sprites=use_last_received_sprites, - pred=pred, not_to=not_to, gag_replaced=gag_replaced, is_staff=is_staff, - in_area=in_area, to_blind=to_blind, to_deaf=to_deaf, - msg=msg, folder=folder, pos=pos, char_id=char_id, ding=ding, color=color, - showname=showname, hide_character=hide_character) + cond = Constants.build_cond(self, is_staff=is_staff, is_officer=is_officer, + in_area=in_area, not_to=not_to.union({self}), + part_of=part_of, to_blind=to_blind, to_deaf=to_deaf, + is_zstaff=is_zstaff, is_zstaff_flex=is_zstaff_flex, + pred=pred) + self.server.make_all_clients_do("send_ic", pred=cond, + params=params, + sender=sender, + bypass_text_replace=bypass_text_replace, + bypass_deafened_starters=bypass_deafened_starters, + use_last_received_sprites=use_last_received_sprites, + gag_replaced=gag_replaced, + msg=msg, + folder=folder, + pos=pos, + char_id=char_id, + ding=ding, + color=color, + showname=showname, + hide_character=hide_character) def send_ic_attention(self): self.send_ic(msg='(Something catches your attention)', ding=1, hide_character=1) diff --git a/server/commands.py b/server/commands.py index 5c242d0e9..53aebaa11 100644 --- a/server/commands.py +++ b/server/commands.py @@ -7986,8 +7986,11 @@ def ooc_cmd_switch(client: ClientManager.Client, arg: str): def ooc_cmd_think(client: ClientManager.Client, arg: str): """ - Sends an IC message that only you (and zone watchers or GMs+ in OOC) can see. - You get to see the IC message with the last sprite you saw. + Sends an IC message that only you can see in IC (a thought), as well as any other players in + your area that are either zone watchers, GMs+ or mind readers. + Zone watchers, GMs+ or mind readers receive a copy of the thought in OOC, regardless of them + having seen the thought because they were in the area or not. + Players who see the thought in IC get to see it with the last sprite they saw. SYNTAX /think @@ -8005,12 +8008,27 @@ def ooc_cmd_think(client: ClientManager.Client, arg: str): msg = arg[:256] client.send_ic(msg=msg, pos=client.pos, folder=client.char_folder, char_id=client.char_id, - showname='[T] ' + client.showname_else_char_showname, hide_character=1, + showname='[T] ' + client.showname_else_char_showname, bypass_text_replace=True, use_last_received_sprites=True) client.send_ooc(f'You thought `{arg}`.') - client.send_ooc_others(f'{client.displayname} [{client.id}] thought `{arg}` ' + + client.send_ic_others(msg=msg, pos=client.pos, folder=client.char_folder, + char_id=client.char_id, + showname='[T] ' + client.showname_else_char_showname, + bypass_text_replace=True, use_last_received_sprites=True, + is_zstaff_flex=True, in_area=True) + client.send_ooc_others(f'(X) {client.displayname} [{client.id}] thought `{arg}` ' f'({client.area.id}).', is_zstaff_flex=True) + client.send_ic_others(msg=msg, pos=client.pos, folder=client.char_folder, + char_id=client.char_id, + showname='[T] ' + client.showname_else_char_showname, + bypass_text_replace=True, use_last_received_sprites=True, + is_zstaff_flex=False, in_area=True, pred=lambda c: c.is_mindreader) + client.send_ooc_others(f'(X) {client.displayname} [{client.id}] thought `{arg}` ' + f'({client.area.id}).', + is_zstaff_flex=False, pred=lambda c: c.is_mindreader) + def ooc_cmd_time(client: ClientManager.Client, arg: str): """ @@ -11268,6 +11286,52 @@ def ooc_cmd_sneakself(client: ClientManager.Client, arg: str): client.send_ooc(f'The following clients could not be sneaked: {s_non_targets}') +def ooc_cmd_mindreader(client: ClientManager.Client, arg: str): + """ (STAFF ONLY) + Toggles a client by ID being a mind reader or not (i.e. can read all thoughts caused by /think, + not just those initiated by the player), or yourself if not given an argument. + Returns an error if the given identifier does not correspond to a user. + + SYNTAX + /mindreader + /mindreader + + OPTIONAL PARAMETERS + {client_id}: Client identifier (number in brackets in /getarea) + + EXAMPLE + Assuming a user with client ID 0 starts as not being a mind reader... + >>> /mindreader 0 + This user can now read all thoughts. + >>> /mindreader 0 + This user can no longer read thoughts not initiated by the user. + """ + + Constants.assert_command(client, arg, is_staff=True, parameters='<2') + + # Invert current mindreader status of matching targets + if not arg: + target = client + else: + target = Constants.parse_id(client, arg) + target.is_mindreader = not target.is_mindreader + + status = {False: 'no longer', True: 'now'} + status2 = {False: 'no longer a', True: 'a'} + if client != target: + client.send_ooc(f'{target.displayname} ({target.id}) is {status[target.is_mindreader]} a ' + f'mind reader.') + client.send_ooc_others(f'(X) {client.displayname} ({client.id}) made {target.displayname} ' + f'({target.id}) be {status2[target.is_mindreader]} mind reader ' + f'({client.area.id}).', is_zstaff_flex=True) + target.send_ooc(f'You are {status[target.is_transient]} a mind reader.') + else: + client.send_ooc(f'You made yourself be {status2[target.is_mindreader]} mind reader.') + client.send_ooc_others(f'(X) {client.displayname} ({client.id}) made themselves be ' + f'{status2[target.is_mindreader]} mind reader ' + f'({client.area.id}).', is_zstaff_flex=True) + + 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 d3eb082fa..bdc2c7e37 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a5' - self.internal_version = 'm220813b' + self.segment_version = 'a6' + self.internal_version = 'm220819a' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) @@ -398,14 +398,16 @@ def load_config(self) -> Dict[str, Any]: def load_characters(self) -> List[str]: characters = ValidateCharacters().validate('config/characters.yaml') - if self.char_list != characters: - # Inconsistent character list, so change everyone to spectator - for client in self.get_clients(): - if client.char_id != -1: - # Except those that are already spectators - client.change_character(-1) - client.send_ooc('The server character list was changed and no longer reflects your ' - 'client character list. Please rejoin the server.') + if self.char_list == characters: + return characters.copy() + + # Inconsistent character list, so change everyone to spectator + for client in self.get_clients(): + if client.char_id != -1: + # Except those that are already spectators + client.change_character(-1) + client.send_ooc('The server character list was changed and no longer reflects your ' + 'client character list. Please rejoin the server.') self.char_list = characters return characters.copy() From fd655de673bce1a6444bfcc2a2df83e168069c06 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Fri, 19 Aug 2022 23:11:28 -0400 Subject: [PATCH 07/13] Fix IC-via-OOC commands sending an empty position if the sender never spoke IC before sending the command --- CHANGELOG.md | 1 + server/client_manager.py | 4 ++-- server/tsuserver.py | 4 ++-- tests/structures.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c2e737e..8f708e58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -788,3 +788,4 @@ * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field * Fixed /think messages not showing the last sent sprite +* Fixed IC-via-OOC commands sending an empty position if the sender never spoke IC before sending the command diff --git a/server/client_manager.py b/server/client_manager.py index 3353ea0e3..0add858fd 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -63,7 +63,7 @@ def __init__(self, server: TsuserverDR, transport, user_id: int, ipid: int, self.name = '' self.char_folder = '' self.char_showname = '' - self.pos = '' + self.pos = 'wit' self.showname = '' self.ever_chose_character = False self.joined = time.time() @@ -833,7 +833,7 @@ def change_character(self, char_id: int, force: bool = False, # Assumes players are not iniswapped initially, waiting for chrini packet self.char_folder = self.get_char_name() self.char_showname = '' - self.pos = '' + self.pos = 'wit' if announce_zwatch: self.send_ooc_others('(X) Client {} has changed from character `{}` to `{}` in ' diff --git a/server/tsuserver.py b/server/tsuserver.py index bdc2c7e37..814805426 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a6' - self.internal_version = 'm220819a' + self.segment_version = 'a7' + self.internal_version = 'm220819b' 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/structures.py b/tests/structures.py index eb122ba74..e365d8ac5 100644 --- a/tests/structures.py +++ b/tests/structures.py @@ -654,7 +654,7 @@ def sic(self, message, msg_type=0, pre='-', folder=None, anim=None, pos=None, sf if anim is None: anim = 'happy' if pos is None: - pos = self.pos if self.pos else 'def' + pos = self.pos if self.pos else 'wit' if char_id is None: char_id = self.char_id if evi is None: From bfbae5d7668c21e3999b3fae38154e6c6180690e Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 20 Aug 2022 10:16:40 -0400 Subject: [PATCH 08/13] Players using Danganronpa Online 1.2.2 now properly adjust to new character lists if the server is refreshed and the character list is changed --- CHANGELOG.md | 1 + server/client_manager.py | 2 +- server/clients.py | 14 ++++++++++++++ server/network/ao_commands.py | 5 ++++- server/tsuserver.py | 32 +++++++++++++++++++++++++------- tests/structures.py | 2 +- 6 files changed, 46 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f708e58e..8026c3501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -785,6 +785,7 @@ * GMs and zone watchers now see thoughts in IC if the thinker is in the same area * Added /mindreader, which allows GMs to let mind readers to see players using /think just like GMs now do * GMs are no longer subject to the duplicate showname checks +* Players using Danganronpa Online 1.2.2 now properly adjust to new character lists if the server is refreshed and the character list is changed * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field * Fixed /think messages not showing the last sent sprite diff --git a/server/client_manager.py b/server/client_manager.py index 0add858fd..db83c7b0b 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -51,7 +51,7 @@ def __init__(self, server: TsuserverDR, transport, user_id: int, ipid: int, self.required_packets_received = set() # Needs to have length 2 to actually connect self.can_askchaa = True # Needs to be true to process an askchaa packet self.version = ('Undefined', 'Undefined') # AO version used established through ID pack - self.packet_handler = clients.ClientDRO1d2d0() + self.packet_handler = clients.ClientDRO1d2d2() self.bad_version = False self.publisher = Publisher(self) diff --git a/server/clients.py b/server/clients.py index e0507e47c..b365e7426 100644 --- a/server/clients.py +++ b/server/clients.py @@ -38,6 +38,7 @@ def __eq__(self, other): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = True ALLOWS_INVISIBLE_BLANKPOSTS = True REPLACES_BASE_OPUS_FOR_MP3 = False + ALLOWS_CHAR_LIST_RELOAD = True DECRYPTOR_OUTBOUND = [ ('key', 34), # 0 @@ -336,12 +337,18 @@ def __eq__(self, other): JOINED_AREA_OUTBOUND = [ ] + +class ClientDRO1d2d2(DefaultDROProtocol): + VERSION_TO_SEND = [1, 2, 2] + class ClientDRO1d2d0(DefaultDROProtocol): VERSION_TO_SEND = [1, 2, 0] + ALLOWS_CHAR_LIST_RELOAD = False class ClientDRO1d1d0(DefaultDROProtocol): VERSION_TO_SEND = [1, 1, 0] HAS_JOINED_AREA = False + ALLOWS_CHAR_LIST_RELOAD = False class ClientDRO1d0d0(DefaultDROProtocol): VERSION_TO_SEND = [1, 0, 0] @@ -350,6 +357,7 @@ class ClientDRO1d0d0(DefaultDROProtocol): HAS_ACKMS = True HAS_JOINED_AREA = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False MS_INBOUND = [ ('msg_type', ArgType.STR), # 0 @@ -403,6 +411,7 @@ class ClientDROLegacy(DefaultDROProtocol): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = False ALLOWS_INVISIBLE_BLANKPOSTS = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False MS_INBOUND = [ ('msg_type', ArgType.STR), # 0 @@ -464,6 +473,7 @@ class ClientAO2d6(DefaultDROProtocol): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = False ALLOWS_INVISIBLE_BLANKPOSTS = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False MS_INBOUND = [ ('msg_type', ArgType.STR), # 0 @@ -538,6 +548,7 @@ class ClientAO2d7(DefaultDROProtocol): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = False ALLOWS_INVISIBLE_BLANKPOSTS = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False MS_INBOUND = [ ('msg_type', ArgType.STR), # 0 @@ -624,6 +635,7 @@ class ClientAO2d8d4(DefaultDROProtocol): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = False ALLOWS_INVISIBLE_BLANKPOSTS = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False MS_INBOUND = [ ('msg_type', ArgType.STR), # 0 @@ -715,6 +727,7 @@ class ClientAO2d9d0(DefaultDROProtocol): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = False ALLOWS_INVISIBLE_BLANKPOSTS = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False ASKCHAA_INBOUND = [ ('ao290doesnotsupportpacketswithnoarguments', ArgType.STR_OR_EMPTY), # 0 @@ -830,6 +843,7 @@ class ClientAO2d10(ClientAO2d9d0): ALLOWS_CLEARING_MODIFIED_MESSAGE_FROM_SELF = False ALLOWS_INVISIBLE_BLANKPOSTS = False REPLACES_BASE_OPUS_FOR_MP3 = True + ALLOWS_CHAR_LIST_RELOAD = False ASKCHAA_INBOUND = [] diff --git a/server/network/ao_commands.py b/server/network/ao_commands.py index 945a4d3a4..1d18daf18 100644 --- a/server/network/ao_commands.py +++ b/server/network/ao_commands.py @@ -138,7 +138,10 @@ def check_client_version(): if software == 'DRO': if major >= 2: - client.packet_handler = clients.ClientDRO1d2d0() + if minor >= 2: + client.packet_handler = clients.ClientDRO1d2d2() + else: + client.packet_handler = clients.ClientDRO1d2d0() elif major >= 1: client.packet_handler = clients.ClientDRO1d1d0() else: diff --git a/server/tsuserver.py b/server/tsuserver.py index 814805426..c542a7261 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a7' - self.internal_version = 'm220819b' + self.segment_version = 'a8' + self.internal_version = 'm220820a' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) @@ -402,12 +402,30 @@ def load_characters(self) -> List[str]: return characters.copy() # Inconsistent character list, so change everyone to spectator + new_chars = {char: num for (num, char) in enumerate(characters)} + for client in self.get_clients(): - if client.char_id != -1: - # Except those that are already spectators - client.change_character(-1) - client.send_ooc('The server character list was changed and no longer reflects your ' - 'client character list. Please rejoin the server.') + target_char_id = -1 + old_char_name = client.get_char_name() + + if client.char_id < 0: + # Do nothing for spectators + pass + elif old_char_name not in new_chars: + # Character no longer exists, so switch to spectator + client.send_ooc('Your character is no longer available. Switching to spectator.') + pass + else: + target_char_id = new_chars[old_char_name] + + if client.packet_handler.ALLOWS_CHAR_LIST_RELOAD: + client.send_command_dict('SC', { + 'chars_ao2_list': characters, + }) + client.change_character(target_char_id, force=True) + else: + client.send_ooc('The server character list was changed and no longer reflects your ' + 'client character list. Please rejoin the server.') self.char_list = characters return characters.copy() diff --git a/tests/structures.py b/tests/structures.py index e365d8ac5..ec3f7467a 100644 --- a/tests/structures.py +++ b/tests/structures.py @@ -782,7 +782,7 @@ def receive_command_stc(self, command_type, *args): if command_type == 'decryptor': # Hi buffer = 'HI#FAKEHDID#%' elif command_type == 'ID': # Server ID - buffer = "ID#DRO#1.2.0#%" + buffer = "ID#DRO#1.2.2#%" err = ('Wrong client ID for {}.\nExpected {}\nGot {}' .format(self, args[0], self.id)) assert args[0] == str(self.id), err From 7b58a33471b3b58aa2dc6970fe722c4faaa24195 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 20 Aug 2022 10:20:14 -0400 Subject: [PATCH 09/13] Update tester to use DRO 1.2.2 --- server/tsuserver.py | 4 ++-- tests/test_client_connection.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/tsuserver.py b/server/tsuserver.py index c542a7261..7844b8725 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a8' - self.internal_version = 'm220820a' + self.segment_version = 'a9' + self.internal_version = 'm220820b' 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 22816caa7..3c1e5d672 100644 --- a/tests/test_client_connection.py +++ b/tests/test_client_connection.py @@ -3,7 +3,7 @@ _standard_FL = ('yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'cccc_ic_support', 'looping_sfx', 'additive', 'effects', 'y_offset', 'ackMS', 'showname', 'chrini', 'charscheck', 'v110') -_standard_client_version = ('1', '2', '0') +_standard_client_version = ('1', '2', '2') class TestClientConnection(_Unittest): From f76bad91c188aa7a589e4465d4cea03807dcde2d Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 20 Aug 2022 14:49:22 -0400 Subject: [PATCH 10/13] Add an extra parameter to /clock_period, allowing GMs to set the hour length of all hours within a period. --- CHANGELOG.md | 1 + README.md | 5 ++-- server/commands.py | 67 +++++++++++++++++++++++++++++---------------- server/tasker.py | 54 ++++++++++++++++++++++++------------ server/tsuserver.py | 4 +-- 5 files changed, 85 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8026c3501..c1c13c127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -786,6 +786,7 @@ * Added /mindreader, which allows GMs to let mind readers to see players using /think just like GMs now do * GMs are no longer subject to the duplicate showname checks * Players using Danganronpa Online 1.2.2 now properly adjust to new character lists if the server is refreshed and the character list is changed +* Added an extra parameter to /clock_period, allowing GMs to set the hour length of all hours within a period. * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field * Fixed /think messages not showing the last sent sprite diff --git a/README.md b/README.md index 64339f1e6..6cbfa48f3 100644 --- a/README.md +++ b/README.md @@ -369,15 +369,16 @@ GMs can: * **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" "hours in a day" - - 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. + - Sets up a day cycle that, starting from the given hour, will tick one hour every given number of seconds (main hour length) and provide a time announcement to a given range of areas. - Hours go from 0 inclusive to the number of hours in a day given exclusive, or 0 to 23 inclusive if not given a number of hours in a day. * **clock_end** "ID" - Ends the day cycle initiated by the target or yourself if not given a target. * **clock_pause** "ID" - Pauses the day cycle initiated by the target or yourself if not given a target. -* **clock_period** "name" "hour start" +* **clock_period** "name" "hour length" "hour start" - Initializes a clock period that starts at the given hour for your day cycle. - Whenever the clock ticks into the period, players in the clock range will be ordered to switch to that time of day's version of their theme. + - If only two arguments are given, the second argument is assumed to be hour start, and hour length is the main hour length. - Clock period names are automatically made all lowercase. * **clock_set** "hour length" "hour" - Modifies the hour length and current hour of your day cycle without restarting it. This is the way to move the day cycle out of unknown time if needed as well. diff --git a/server/commands.py b/server/commands.py index 53aebaa11..3a736c623 100644 --- a/server/commands.py +++ b/server/commands.py @@ -1537,15 +1537,15 @@ def ooc_cmd_clock(client: ClientManager.Client, arg: str): Requires /clock_end to undo. Returns an error if the given hour start is not a nonnegative number or beyond the indicated number of hours in a day, if the number of hours in a day is not a positive integer, or if the - hour length is not a positive number. + hour length is not a positive integer. SYNTAX - /clock {hours_in_day} + /clock {hours_in_day} PARAMETERS : Send notifications from this area onwards up to... : Send notifications up to (and including) this area. - : Length of each ingame hour (in seconds) + : Main length of each ingame hour (in seconds) : Starting hour (integer from 0 to 23) OPTIONAL PARAMETERS @@ -1699,61 +1699,80 @@ def ooc_cmd_clock_pause(client: ClientManager.Client, arg: str): def ooc_cmd_clock_period(client: ClientManager.Client, arg: str): """ (STAFF ONLY) Adds a period to the day cycle you established. Whenever the day cycle clock ticks - into a time part of the period, all clients in the affected areas will be ordered to change - to that time of day's version of their theme. Time of day periods go from their given hour - start all the way until the next period. + into a time part of the period after a given hour length of seconds, all clients in the + affected areas will be ordered to change to that time of day's version of their theme. + Time of day periods go from their given hour start all the way until the next period. If the period name already exists, its hour start will be overwritten. + If the hour length is not given, it will use the main hour length of the day cycle. If some period already starts at the given hour start, its name will be overwritten. Returns an error if you have not started a day cycle, or if the hour start is not an integer - from 0 inclusive to the number of hours in a day set up for the day cycle exclusive. + from 0 inclusive to the number of hours in a day set up for the day cycle exclusive, or if given + an hour length and it is not a positive integer. SYNTAX /clock_period + /clock_period {hour_length} PARAMETERS : Name of the period. : Start time of the period. + OPTIONAL PARAMETERS + {hour_length}: Length of each ingame hour (in seconds). Defaults to the main hour length. + EXAMPLE Assuming the commands are run in order... >>> /clock_period day 8 Sets up a period that goes from 8 AM to 8 AM. - >>> /clock_period night 22 - Sets up a night period that goes from 10 PM to 8 AM. Day period now goes from 8 AM to 10 PM. + >>> /clock_period night 150 22 + Sets up a night period that goes from 10 PM to 8 AM, each hour in the period ticking every 150 + seconds. Day period now goes from 8 AM to 10 PM. """ - Constants.assert_command(client, arg, is_staff=True, parameters='&1-2') + Constants.assert_command(client, arg, is_staff=True, parameters='&1-3') try: task = client.server.tasker.get_task(client, ['as_day_cycle']) except KeyError: raise ClientError('You have not initiated any day cycles.') + args = arg.split() + hour_length = client.server.tasker.get_task_attr(client, ['as_day_cycle'], + 'main_hour_length') + hours_in_day = client.server.tasker.get_task_attr(client, ['as_day_cycle'], + 'hours_in_day') + + name = args[0].lower() + pre_hour_start = args[2] if len(args) == 3 else (args[1] if len(args) == 2 else "-1") + pre_hour_length = args[1] if len(args) == 3 else (str(hour_length) if len(args) == 2 else "1") + try: - args = arg.split() - if len(args) == 1: - name, pre_start, start = args[0].lower(), "-1", -1 - else: - name, pre_start = args[0].lower(), args[1] - start = int(pre_start) # Do it separately so ValueError exception may read args[1] - hours_in_day = client.server.tasker.get_task_attr(client, ['as_day_cycle'], - 'hours_in_day') - if not 0 <= start < hours_in_day: - start = args[1] - raise ValueError + hour_start = int(pre_hour_start) + if not -1 <= hour_start < hours_in_day: + raise ValueError + except ValueError: + raise ArgumentError(f'Invalid period start hour {hour_start}.') + + try: + hour_length = int(pre_hour_length) + if hour_length <= 0: + raise ValueError except ValueError: - raise ArgumentError('Invalid period start hour {}.'.format(pre_start)) + raise ArgumentError(f'Invalid period hour length {hour_length}.') - client.server.tasker.set_task_attr(client, ['as_day_cycle'], 'new_period_start', (start, name)) + client.server.tasker.set_task_attr(client, ['as_day_cycle'], 'new_period_start', + (hour_start, name, hour_length)) client.server.tasker.set_task_attr(client, ['as_day_cycle'], 'refresh_reason', 'period') client.server.tasker.cancel_task(task) def ooc_cmd_clock_set(client: ClientManager.Client, arg: str): """ (STAFF ONLY) - Updates the hour length and current hour of the client's day cycle without restarting it, + Updates the main hour length and current hour of the client's day cycle without restarting it, changing its area range or notifying normal players. If the day cycle time was unknown, the time is updated in the same manner (effectively taking it out of unknown mode). + The hour length of any active periods are not modified and have priority over the main hour + length. Returns an error if you have not started a day cycle, or if the hour is not an integer from 0 inclusive to the number of hours in a day set up for the day cycle exclusive. diff --git a/server/tasker.py b/server/tasker.py index 0d11daeba..cd4c78e8d 100644 --- a/server/tasker.py +++ b/server/tasker.py @@ -25,17 +25,18 @@ import time import typing -from typing import Any, List +from typing import Any, List, Tuple from server.constants import Constants from server.exceptions import ServerError if typing.TYPE_CHECKING: from server.client_manager import ClientManager + from server.tsuserver import TsuserverDR class Tasker: - def __init__(self, server): + def __init__(self, server: TsuserverDR): """ Parameters ---------- @@ -256,11 +257,12 @@ async def as_day_cycle(self, client: ClientManager.Client, args: List): _, area_1, area_2, hour_length, hour_start, hours_in_day, send_first_hour = args hour = hour_start minute_at_interruption = 0 + main_hour_length = hour_length time_started_at = time.time() time_refreshed_at = time.time() # Doesnt need init, but PyLint complains otherwise periods = list() force_period_refresh = False - current_period = (-1, '') + current_period = (-1, '', main_hour_length) notify_normies = False # Initialize task attributes @@ -269,6 +271,7 @@ async def as_day_cycle(self, client: ClientManager.Client, args: List): self.set_task_attr(client, ['as_day_cycle'], 'refresh_reason', '') self.set_task_attr(client, ['as_day_cycle'], 'period', '') self.set_task_attr(client, ['as_day_cycle'], 'hours_in_day', hours_in_day) + self.set_task_attr(client, ['as_day_cycle'], 'main_hour_length', main_hour_length) # Manually notify for the very first hour (if needed) targets = [c for c in self.server.get_clients() if c == client or @@ -277,14 +280,14 @@ async def as_day_cycle(self, client: ClientManager.Client, args: List): c.send_ooc('It is now {}:00.'.format('{0:02d}'.format(hour))) c.send_clock(client_id=client.id, hour=hour) - def find_period_of_hour(hour): + def find_period_of_hour(hour) -> Tuple[int, str, int]: if not periods: - return (-1, '') + return (-1, '', main_hour_length) if hour < periods[0][0]: return periods[-1] output = None for period_tuple in periods: - period_start, _ = period_tuple + period_start, _, _ = period_tuple if hour >= period_start: output = period_tuple return output @@ -341,10 +344,11 @@ def find_period_of_hour(hour): c.send_time_of_day(name='') c.send_ooc(f'It is no longer some particular period of day.') current_period = find_period_of_hour(hour) - new_period_start, new_period_name = current_period + hour_length = main_hour_length else: current_period = find_period_of_hour(hour) - new_period_start, new_period_name = current_period + new_period_start, new_period_name, new_period_length = current_period + hour_length = new_period_length if new_period_start == hour or force_period_refresh: for c in targets: self.set_task_attr(client, ['as_day_cycle'], 'period', new_period_name) @@ -396,6 +400,10 @@ def find_period_of_hour(hour): old_hour = hour hour_length, hour = self.get_task_attr(client, ['as_day_cycle'], 'new_day_cycle_args') + main_hour_length = hour_length + self.set_task_attr(client, ['as_day_cycle'], 'main_hour_length', + main_hour_length) + # Do not notify of clock set to normies if only hour length changed notify_normies = (old_hour != hour) minute_at_interruption = 0 @@ -419,6 +427,14 @@ def find_period_of_hour(hour): # So preemptively -1 if not self.get_task_attr(client, ['as_day_cycle'], 'is_paused'): hour -= 1 # Take one hour away, because an hour would be added anyway + + # This does not modify the hour length of active periods, so if there are any + # periods, warn clock master + if periods: + client.send_ooc('(X) Warning: A period is currently active, so the day ' + 'cycle is using its hour length. Modify its hour length ' + 'using /clock_period.') + elif refresh_reason == 'set_hours': old_hour = hour self.set_task_attr(client, ['as_day_cycle'], 'refresh_reason', @@ -504,15 +520,15 @@ def find_period_of_hour(hour): if not self.get_task_attr(client, ['as_day_cycle'], 'is_paused'): minute_at_interruption += (time_refreshed_at-time_started_at)/hour_length*60 time_started_at = time.time() - start, name = self.get_task_attr(client, ['as_day_cycle'], 'new_period_start') + start, name, length = self.get_task_attr(client, ['as_day_cycle'], 'new_period_start') # Pop entries with same start or name if needed (duplicated entries) - for (entry_start, entry_name) in periods.copy(): + for (entry_start, entry_name, entry_length) in periods.copy(): if entry_start == start or entry_name == name: - periods.remove((entry_start, entry_name)) + periods.remove((entry_start, entry_name, entry_length)) if start >= 0: - periods.append((start, name)) + periods.append((start, name, length)) # start=-1 is used to indicate *please erase this period name*. By the previous # for loop, any matching period names are removed, and by the if statement @@ -528,9 +544,9 @@ def find_period_of_hour(hour): # then no updates should be sent changed_current_period = False if not self.get_task_attr(client, ['as_day_cycle'], 'is_unknown'): - new_period_start, new_period_name = find_period_of_hour(hour) + new_period_start, new_period_name, new_period_length = find_period_of_hour(hour) changed_current_period = (current_period[1] != new_period_name) - current_period = new_period_start, new_period_name + current_period = new_period_start, new_period_name, new_period_length if periods and new_period_start == hour: changed_current_period = True if changed_current_period: @@ -550,11 +566,13 @@ def find_period_of_hour(hour): if start >= 0: # Case added a period formatted_time = '{}:00'.format('{0:02d}'.format(start)) - client.send_ooc(f'(X) You have added period `{name}` that starts at ' - f'{formatted_time}.') + client.send_ooc(f'(X) You have added period `{name}`. ' + f'Period hour length: {new_period_length} seconds. ' + f'Period hour start: {formatted_time}.') client.send_ooc_others(f'(X) {client.displayname} [{client.id}] has ' - f'added period `{name}` to their day cycle that ' - f'starts at {formatted_time} ' + f'added period `{name}` to their day cycle. ' + f'Period hour length: {new_period_length} seconds. ' + f'Period hour start: {formatted_time} ' f'({client.area.id}).', is_zstaff_flex=True) else: diff --git a/server/tsuserver.py b/server/tsuserver.py index 7844b8725..5edcc44c0 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a9' - self.internal_version = 'm220820b' + self.segment_version = 'a10' + self.internal_version = 'm220820c' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) From e0ba39ab69f23bd2363b016f8e39b7782e6d414e Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 20 Aug 2022 17:19:33 -0400 Subject: [PATCH 11/13] Add /clock_period_end --- CHANGELOG.md | 3 +- README.md | 2 + server/commands.py | 47 +++++++++++++++--- server/tasker.py | 115 +++++++++++++++++++++++--------------------- server/tsuserver.py | 4 +- 5 files changed, 106 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c13c127..d4fa0f6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -786,7 +786,8 @@ * Added /mindreader, which allows GMs to let mind readers to see players using /think just like GMs now do * GMs are no longer subject to the duplicate showname checks * Players using Danganronpa Online 1.2.2 now properly adjust to new character lists if the server is refreshed and the character list is changed -* Added an extra parameter to /clock_period, allowing GMs to set the hour length of all hours within a period. +* Added an extra parameter to /clock_period, allowing GMs to set the hour length of all hours within a period +* Added an explicit /clock_period_end to delete a clock period, rather than relying on an undocumented 1-parameter call to /clock_period * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field * Fixed /think messages not showing the last sent sprite diff --git a/README.md b/README.md index 6cbfa48f3..ceba6343b 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,8 @@ GMs can: - Whenever the clock ticks into the period, players in the clock range will be ordered to switch to that time of day's version of their theme. - If only two arguments are given, the second argument is assumed to be hour start, and hour length is the main hour length. - Clock period names are automatically made all lowercase. +* **clock_period_end** "name" + - Deletes a clock period for your day cycle. * **clock_set** "hour length" "hour" - Modifies the hour length and current hour of your day cycle without restarting it. This is the way to move the day cycle out of unknown time if needed as well. - Acts just like doing /clock again, but does not erase already set periods. diff --git a/server/commands.py b/server/commands.py index 3a736c623..fd2a70f55 100644 --- a/server/commands.py +++ b/server/commands.py @@ -1729,7 +1729,7 @@ def ooc_cmd_clock_period(client: ClientManager.Client, arg: str): seconds. Day period now goes from 8 AM to 10 PM. """ - Constants.assert_command(client, arg, is_staff=True, parameters='&1-3') + Constants.assert_command(client, arg, is_staff=True, parameters='&2-3') try: task = client.server.tasker.get_task(client, ['as_day_cycle']) @@ -1737,18 +1737,16 @@ def ooc_cmd_clock_period(client: ClientManager.Client, arg: str): raise ClientError('You have not initiated any day cycles.') args = arg.split() - hour_length = client.server.tasker.get_task_attr(client, ['as_day_cycle'], - 'main_hour_length') - hours_in_day = client.server.tasker.get_task_attr(client, ['as_day_cycle'], - 'hours_in_day') + hour_length = client.server.tasker.get_task_attr(client, ['as_day_cycle'], 'main_hour_length') + hours_in_day = client.server.tasker.get_task_attr(client, ['as_day_cycle'], 'hours_in_day') name = args[0].lower() - pre_hour_start = args[2] if len(args) == 3 else (args[1] if len(args) == 2 else "-1") - pre_hour_length = args[1] if len(args) == 3 else (str(hour_length) if len(args) == 2 else "1") + pre_hour_start = args[2] if len(args) == 3 else args[1] + pre_hour_length = args[1] if len(args) == 3 else str(hour_length) try: hour_start = int(pre_hour_start) - if not -1 <= hour_start < hours_in_day: + if not 0 <= hour_start < hours_in_day: raise ValueError except ValueError: raise ArgumentError(f'Invalid period start hour {hour_start}.') @@ -1766,6 +1764,39 @@ def ooc_cmd_clock_period(client: ClientManager.Client, arg: str): client.server.tasker.cancel_task(task) +def ooc_cmd_clock_period_end(client: ClientManager.Client, arg: str): + """ (STAFF ONLY) + Removes a previously created period to the day cycle you established. + If the removed period is the one currently active, the period becomes whatever the new period + should be using the remaining periods if there are any, or fully deactivated if there are no + other periods left. + Returns an error if you have not started a day cycle, or if the period does not exist. + + SYNTAX + /clock_period_end + + PARAMETERS + : Name of the period. + + EXAMPLE + Assuming the commands are run in order... + >>> /clock_period_end day + Removes the period called day. + """ + + Constants.assert_command(client, arg, is_staff=True, parameters='=1') + + try: + task = client.server.tasker.get_task(client, ['as_day_cycle']) + except KeyError: + raise ClientError('You have not initiated any day cycles.') + + client.server.tasker.set_task_attr(client, ['as_day_cycle'], 'new_period_start', + (-1, arg, 0)) + client.server.tasker.set_task_attr(client, ['as_day_cycle'], 'refresh_reason', 'period') + client.server.tasker.cancel_task(task) + + def ooc_cmd_clock_set(client: ClientManager.Client, arg: str): """ (STAFF ONLY) Updates the main hour length and current hour of the client's day cycle without restarting it, diff --git a/server/tasker.py b/server/tasker.py index cd4c78e8d..5931809e2 100644 --- a/server/tasker.py +++ b/server/tasker.py @@ -523,65 +523,72 @@ def find_period_of_hour(hour) -> Tuple[int, str, int]: start, name, length = self.get_task_attr(client, ['as_day_cycle'], 'new_period_start') # Pop entries with same start or name if needed (duplicated entries) + found = False for (entry_start, entry_name, entry_length) in periods.copy(): if entry_start == start or entry_name == name: periods.remove((entry_start, entry_name, entry_length)) + found = True - if start >= 0: - periods.append((start, name, length)) - - # start=-1 is used to indicate *please erase this period name*. By the previous - # for loop, any matching period names are removed, and by the if statement - # -1 is not added. - - periods.sort() - # Decide which period the current hour belongs to - # Note it could be possible the current hour is smaller than than the first - # period start. By wrapping around 24 hours logic, that means the current - # period is the one given by the latest period. - - # Also note this is only relevant if the time is not unknown. If it is, - # then no updates should be sent - changed_current_period = False - if not self.get_task_attr(client, ['as_day_cycle'], 'is_unknown'): - new_period_start, new_period_name, new_period_length = find_period_of_hour(hour) - changed_current_period = (current_period[1] != new_period_name) - current_period = new_period_start, new_period_name, new_period_length - if periods and new_period_start == hour: - changed_current_period = True - if changed_current_period: - targets = [c for c in self.server.get_clients() - if c == client or area_1 <= c.area.id <= area_2] - self.set_task_attr(client, ['as_day_cycle'], 'period', new_period_name) - if new_period_name: - for c in targets: - c.send_time_of_day(name=new_period_name) - c.send_ooc(f'It is now {new_period_name}.') - else: - for c in targets: - c.send_time_of_day(name='') - c.send_ooc(f'It is no longer some particular period of day.') - - # Send notifications appropriately - if start >= 0: - # Case added a period - formatted_time = '{}:00'.format('{0:02d}'.format(start)) - client.send_ooc(f'(X) You have added period `{name}`. ' - f'Period hour length: {new_period_length} seconds. ' - f'Period hour start: {formatted_time}.') - client.send_ooc_others(f'(X) {client.displayname} [{client.id}] has ' - f'added period `{name}` to their day cycle. ' - f'Period hour length: {new_period_length} seconds. ' - f'Period hour start: {formatted_time} ' - f'({client.area.id}).', - is_zstaff_flex=True) + if not found and start < 0: + # Check if attempted to remove a non-existing period + client.send_ooc(f'Period `{name}` not found.') else: - # Case removed a period - client.send_ooc(f'(X) You have removed period `{name}`.') - client.send_ooc_others(f'(X) {client.displayname} [{client.id}] has ' - f'removed period `{name}` off their day cycle ' - f'({client.area.id}).', - is_zstaff_flex=True) + if start >= 0: + periods.append((start, name, length)) + + # start=-1 is used to indicate *please erase this period name*. By the previous + # for loop, any matching period names are removed, and by the if statement + # -1 is not added. + + periods.sort() + # Decide which period the current hour belongs to + # Note it could be possible the current hour is smaller than than the first + # period start. By wrapping around 24 hours logic, that means the current + # period is the one given by the latest period. + + # Also note this is only relevant if the time is not unknown. If it is, + # then no updates should be sent + changed_current_period = False + if not self.get_task_attr(client, ['as_day_cycle'], 'is_unknown'): + new_period_start, new_period_name, new_period_length = find_period_of_hour(hour) + changed_current_period = (current_period[1] != new_period_name) + current_period = new_period_start, new_period_name, new_period_length + if periods and new_period_start == hour: + changed_current_period = True + + if changed_current_period: + targets = [c for c in self.server.get_clients() + if c == client or area_1 <= c.area.id <= area_2] + self.set_task_attr(client, ['as_day_cycle'], 'period', new_period_name) + if new_period_name: + for c in targets: + c.send_time_of_day(name=new_period_name) + c.send_ooc(f'It is now {new_period_name}.') + else: + for c in targets: + c.send_time_of_day(name='') + c.send_ooc(f'It is no longer some particular period of day.') + + # Send notifications appropriately + if start >= 0: + # Case added a period + formatted_time = '{}:00'.format('{0:02d}'.format(start)) + client.send_ooc(f'(X) You have added period `{name}`. ' + f'Period hour length: {new_period_length} seconds. ' + f'Period hour start: {formatted_time}.') + client.send_ooc_others(f'(X) {client.displayname} [{client.id}] has ' + f'added period `{name}` to their day cycle. ' + f'Period hour length: {new_period_length} seconds. ' + f'Period hour start: {formatted_time} ' + f'({client.area.id}).', + is_zstaff_flex=True) + else: + # Case removed a period + client.send_ooc(f'(X) You have removed period `{name}`.') + client.send_ooc_others(f'(X) {client.displayname} [{client.id}] has ' + f'removed period `{name}` off their day cycle ' + f'({client.area.id}).', + is_zstaff_flex=True) elif refresh_reason == 'unpause': self.set_task_attr(client, ['as_day_cycle'], 'is_paused', False) diff --git a/server/tsuserver.py b/server/tsuserver.py index 5edcc44c0..07d260f73 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a10' - self.internal_version = 'm220820c' + self.segment_version = 'a11' + self.internal_version = 'm220820d' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version) From 0500234c948361a02edfd2f89406b8b0d117a2d2 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sat, 20 Aug 2022 20:35:43 -0400 Subject: [PATCH 12/13] Fix sending duplicate empty TOD and GM packets --- server/client_changearea.py | 4 +++- server/client_manager.py | 36 +++++++-------------------------- server/network/ao_commands.py | 14 +++++++------ server/tsuserver.py | 4 ++-- tests/test_client_connection.py | 12 +++++------ 5 files changed, 26 insertions(+), 44 deletions(-) diff --git a/server/client_changearea.py b/server/client_changearea.py index 7772e4484..0ed2fc123 100644 --- a/server/client_changearea.py +++ b/server/client_changearea.py @@ -834,11 +834,13 @@ def post_area_changed(self, old_area: Union[None, AreaManager.Area], area: AreaM client.send_health(side=1, health=client.area.hp_def) client.send_health(side=2, health=client.area.hp_pro) + new_area_clock_period = area.get_clock_period() if old_area: old_area_clock_period = old_area.get_clock_period() - new_area_clock_period = area.get_clock_period() if old_area_clock_period != new_area_clock_period: client.send_time_of_day(name=new_area_clock_period) + else: + client.send_time_of_day(name=new_area_clock_period) if client.is_blind: client.send_background(name=client.server.config['blackout_background']) diff --git a/server/client_manager.py b/server/client_manager.py index db83c7b0b..9900b9343 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -65,10 +65,13 @@ def __init__(self, server: TsuserverDR, transport, user_id: int, ipid: int, self.char_showname = '' self.pos = 'wit' self.showname = '' - self.ever_chose_character = False self.joined = time.time() self.last_active = Constants.get_time() + self.ever_chose_character = False + self.ever_outbounded_gamemode = False + self.ever_outbounded_time_of_day = False + self.area = server.area_manager.default_area() self.new_area = self.area # It is different from self.area in transition to a new area self.party = None @@ -679,11 +682,13 @@ def send_clock(self, client_id=None, hour=None): }) def send_gamemode(self, name=None): + self.ever_outbounded_gamemode = True self.send_command_dict('GM', { 'name': name, }) def send_time_of_day(self, name=None): + self.ever_outbounded_time_of_day = True self.send_command_dict('TOD', { 'name': name, }) @@ -1619,30 +1624,7 @@ def refresh_visible_char_list(self): def send_done(self): self.refresh_visible_char_list() self.post_area_changed(None, self.area) - """ - self.send_command_dict('HP', { - 'side': 1, - 'health': self.area.hp_def - }) - self.send_command_dict('HP', { - 'side': 2, - 'health': self.area.hp_pro - }) - if self.is_blind: - self.send_background(name=self.server.config['blackout_background']) - else: - self.send_background(name=self.area.background, - tod_backgrounds=self.area.get_background_tod()) - self.send_command_dict('LE', { - 'evidence_ao2_list': self.area.get_evidence_list(self), - }) - self.send_command_dict('MM', { - 'unknown': 1, - }) - self.send_command_dict('OPPASS', { - 'guard_pass': '', - }) - """ + if self.char_id is None: self.char_id = -1 # Set to a valid ID if still needed self.send_command_dict('DONE', dict()) @@ -1651,10 +1633,6 @@ def send_done(self): self.send_ooc(f'Unknown client detected {self.version}. ' f'Assuming standard DRO client protocol.') - if self.bad_version: - self.send_ooc(f'Unknown client detected {self.version}. ' - f'Assuming standard DRO client protocol.') - def char_select(self): # By running the change_character code, all checks and actions for switching to # spectator are made diff --git a/server/network/ao_commands.py b/server/network/ao_commands.py index 1d18daf18..e3c74120e 100644 --- a/server/network/ao_commands.py +++ b/server/network/ao_commands.py @@ -323,12 +323,14 @@ def net_cmd_cc(client: ClientManager.Client, pargs: Dict[str, Any]): client.last_active = Constants.get_time() if not ever_chose_character_before: - client.send_command_dict('GM', { - 'name': '' - }) - client.send_command_dict('TOD', { - 'name': '' - }) + if not client.ever_outbounded_gamemode: + client.send_command_dict('GM', { + 'name': '' + }) + if not client.ever_outbounded_time_of_day: + client.send_command_dict('TOD', { + 'name': '' + }) try: client.area.play_current_track(only_for={client}, force_same_restart=1) except AreaError: diff --git a/server/tsuserver.py b/server/tsuserver.py index 07d260f73..20cd2ae28 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'a11' - self.internal_version = 'm220820d' + self.segment_version = 'b1' + self.internal_version = 'm220820e' 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 3c1e5d672..8fccf2885 100644 --- a/tests/test_client_connection.py +++ b/tests/test_client_connection.py @@ -92,6 +92,7 @@ def test_04_client0_joinserver(self): c.assert_packet('CharsCheck', None) c.assert_packet('HP', (1, 10)) c.assert_packet('HP', (2, 10)) + c.assert_packet('TOD', None) c.assert_packet('BN', None) c.assert_packet('LE', tuple()) c.assert_packet('joined_area', None) @@ -141,6 +142,7 @@ def test_05_client1_joinandpickchar(self): c.assert_packet('CharsCheck', None) c.assert_packet('HP', (1, 10)) c.assert_packet('HP', (2, 10)) + c.assert_packet('TOD', None) c.assert_packet('BN', None) c.assert_packet('LE', tuple()) c.assert_packet('joined_area', None) @@ -160,8 +162,7 @@ def test_05_client1_joinandpickchar(self): # Only now pick char c.send_command_cts("CC#1#0#FAKEHDID#%") # Pick char 0 c.assert_packet('PV', (1, 'CID', 0)) # 1 because second client online - c.assert_packet('GM', '') - c.assert_packet('TOD', '', over=True) + c.assert_packet('GM', '', over=True) assert(c.get_char_name() == self.server.char_list[0]) # Check number of clients @@ -198,6 +199,7 @@ def test_06_client2_joinandpicksamechar(self): c.assert_packet('CharsCheck', None) c.assert_packet('HP', (1, 10)) c.assert_packet('HP', (2, 10)) + c.assert_packet('TOD', None) c.assert_packet('BN', None) c.assert_packet('LE', tuple()) c.assert_packet('joined_area', None) @@ -219,8 +221,7 @@ def test_06_client2_joinandpicksamechar(self): c.assert_no_packets() # Should not happen as client 1 has char 0 c.send_command_cts("CC#2#1#FAKEHDID#%") # Attempt to pick char 1 c.assert_packet('PV', (2, 'CID', 1)) # 2 because third client online - c.assert_packet('GM', '') - c.assert_packet('TOD', '', over=True) + c.assert_packet('GM', '', over=True) assert(c.get_char_name() == self.server.char_list[1]) # Check number of clients @@ -244,8 +245,7 @@ def test_07_client0_pickchar(self): c.assert_no_packets() # Should not happen as there is no char 4 c.send_command_cts("CC#0#3#FAKEHDID#%") # Attempt to pick char 3 c.assert_packet('PV', (0, 'CID', 3)) # 0 because first client online - c.assert_packet('GM', '') - c.assert_packet('TOD', '', over=True) + c.assert_packet('GM', '', over=True) assert(c.get_char_name() == self.server.char_list[3]) self.assertEqual(len(self.server.client_manager.clients), 3) From f1f74aa636998d6daf4096aa151d8c0b434e93d5 Mon Sep 17 00:00:00 2001 From: Chrezm Date: Sun, 21 Aug 2022 10:01:15 -0400 Subject: [PATCH 13/13] Remove ding sound effects for non-noteworthy events --- CHANGELOG.md | 3 ++- README.md | 28 +++++++++++----------- server/area_manager.py | 15 +++++++----- server/client_changearea.py | 43 +++++++++++++++++++--------------- server/client_manager.py | 46 ++++++++++++++++++++----------------- server/commands.py | 6 ++--- server/tsuserver.py | 4 ++-- 7 files changed, 80 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4fa0f6c8..c628b31c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -780,7 +780,7 @@ ### 220723a (4.3.3-post2) * Fixed /zone_ambient not allowing tracks with spaces -### (4.3.4) +### 220821a (4.3.4) * Added /sneakself, which sneaks all of your active multiclients that are not currently sneaked but can be sneaked * GMs and zone watchers now see thoughts in IC if the thinker is in the same area * Added /mindreader, which allows GMs to let mind readers to see players using /think just like GMs now do @@ -788,6 +788,7 @@ * Players using Danganronpa Online 1.2.2 now properly adjust to new character lists if the server is refreshed and the character list is changed * Added an extra parameter to /clock_period, allowing GMs to set the hour length of all hours within a period * Added an explicit /clock_period_end to delete a clock period, rather than relying on an undocumented 1-parameter call to /clock_period +* Ding sound effects are no longer included with "Something catches your attention" IC messages, except those that are triggered because an area was marked as noteworthy * Fixed area validator not properly handling areas with empty names or backgrounds * Fixed typo in area_templates.yaml field * Fixed /think messages not showing the last sent sprite diff --git a/README.md b/README.md index ceba6343b..857fe8ac8 100644 --- a/README.md +++ b/README.md @@ -52,22 +52,22 @@ It is highly recommended you read through all the installation steps first befor * If everything was set up correctly, you will see something like this appear: ``` -[2022-07-17T10:20:20]: Starting... -[2022-07-17T10:20:20]: Launching TsuserverDR 4.3.3 (220717a)... -[2022-07-17T10:20:20]: Loading server configurations... -[2022-07-17T10:20:20]: Server configurations loaded successfully! -[2022-07-17T10:20:20]: Starting a nonlocal server... -[2022-07-17T10:20:20]: Server started successfully! -[2022-07-17T10:20:21]: Server should be now accessible from 192.0.2.0:50000:My First DR Server +[2022-08-21T10:20:20]: Starting... +[2022-08-21T10:20:20]: Launching TsuserverDR 4.3.4 (220821a)... +[2022-08-21T10:20:20]: Loading server configurations... +[2022-08-21T10:20:20]: Server configurations loaded successfully! +[2022-08-21T10:20:20]: Starting a nonlocal server... +[2022-08-21T10:20:20]: Server started successfully! +[2022-08-21T10:20:21]: Server should be now accessible from 192.0.2.0:50000:My First DR Server ``` * If you are listing your server in the Attorney Online master server, make sure its details are set up correctly. In particular, make sure that your server name and description are correct, as that is how players will find your server. If everything was set up correctly, you will see something like this appear: ``` -[2022-07-17T10:20:21]: Attempting to connect to the master server at https://servers.aceattorneyonline.com/servers with the following details: -[2022-07-17T10:20:21]: *Server name: My First DR Server -[2022-07-17T10:20:21]: *Server description: This is my flashy new DR server -[2022-07-17T10:20:22]: Connected to the master server. +[2022-08-21T10:20:21]: Attempting to connect to the master server at https://servers.aceattorneyonline.com/servers with the following details: +[2022-08-21T10:20:21]: *Server name: My First DR Server +[2022-08-21T10:20:21]: *Server description: This is my flashy new DR server +[2022-08-21T10:20:22]: Connected to the master server. ``` - The server will make a single ping to [ipify](https://api.ipify.org) in order to obtain its public IP address. If it fails to do that, it will let you know that, as it means there is probably something wrong with your internet connection and that other players may not be able to connect to your server. @@ -76,9 +76,9 @@ It is highly recommended you read through all the installation steps first befor * To stop the server, press Ctrl+C once from your terminal. This will initiate a shutdown sequence and notify you when it is done. If the shutdown finished successfully, you will see something like this appear: ``` -[2022-07-17T22:23:04]: You have initiated a server shut down. -[2022-07-17T22:23:04]: Kicking 12 remaining clients. -[2022-07-17T22:23:04]: Server has successfully shut down. +[2022-08-21T22:23:04]: You have initiated a server shut down. +[2022-08-21T22:23:04]: Kicking 12 remaining clients. +[2022-08-21T22:23:04]: Server has successfully shut down. ``` * If you do not see anything after a few seconds of starting a shutdown, you can try spamming Ctrl+C to try and force a shutdown or directly close out your terminal. This is not recommended due to the cleanup process not finishing correctly but it is doable. diff --git a/server/area_manager.py b/server/area_manager.py index 4203209ce..27a49b525 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -242,7 +242,8 @@ def broadcast_ooc(self, msg: str): for client in self.clients: client.send_ooc(msg) - def broadcast_ic_attention(self, cond: Callable[[ClientManager.Client], bool] = None): + def broadcast_ic_attention(self, cond: Callable[[ClientManager.Client], bool] = None, + ding: bool = True): """ Send an IC message with a ding to everyone in the area indicating something catches their attention, *except* if the player is blind or deaf, or if the area is a lobby @@ -253,6 +254,8 @@ def broadcast_ic_attention(self, cond: Callable[[ClientManager.Client], bool] = cond : types.LambdaType: ClientManager.Client -> bool, optional Custom condition each player in the area must also satisfy to receive the attention message. + ding : bool, optional + If the accompanying IC message should also include the "ding" effect. Returns ------- @@ -264,13 +267,13 @@ def broadcast_ic_attention(self, cond: Callable[[ClientManager.Client], bool] = return if cond is None: - cond = lambda client: True + cond = lambda _: True for player in self.clients: if player.is_deaf and player.is_blind: continue if cond(player): - player.send_ic_attention() + player.send_ic_attention(ding=ding) def get_background_tod(self) -> Dict[str, str]: if not self.lights: @@ -643,10 +646,10 @@ def change_lights(self, new_lights: bool, initiator: ClientManager.Client = None party.check_lights() for c in self.clients: - found_something = c.area_changer.notify_me_rp(self, changed_visibility=True, - changed_hearing=False) + found_something, ding_something = c.area_changer.notify_me_rp( + self, changed_visibility=True, changed_hearing=False) if found_something and new_lights: - c.send_ic_attention() + c.send_ic_attention(ding=ding_something) def set_next_msg_delay(self, msg_length: int): """ diff --git a/server/client_changearea.py b/server/client_changearea.py index 0ed2fc123..203972a83 100644 --- a/server/client_changearea.py +++ b/server/client_changearea.py @@ -111,7 +111,7 @@ def check_change_area(self, area: AreaManager.Area, def notify_change_area(self, area: AreaManager.Area, old_dname: str, ignore_bleeding: bool = False, ignore_autopass: bool = False, - just_me: bool = False) -> bool: + just_me: bool = False) -> Tuple[bool, bool]: """ Send all OOC notifications that come from switching areas. Right now there is @@ -133,20 +133,23 @@ def notify_change_area(self, area: AreaManager.Area, old_dname: str, If just_me is True, no notifications are sent to other players in the area. - Returns True if any RP related notifications are sent to the player who changed areas, - False otherwise. - + Returns a tuple of two bool arguments: + 0. True if any RP related notifications are sent to the player who changed areas, False + otherwise. + 1. True if such RP related notifications to be sent should include a "ding" effect, False + otherwise. """ - found_something = self.notify_me(area, old_dname, ignore_bleeding=ignore_bleeding) + found_something, ding_something = self.notify_me(area, old_dname, + ignore_bleeding=ignore_bleeding) if not just_me: self.notify_others(area, old_dname, ignore_bleeding=ignore_bleeding, ignore_autopass=ignore_autopass) - return found_something + return found_something, ding_something def notify_me(self, area: AreaManager.Area, old_dname: str, - ignore_bleeding: bool = False) -> bool: + ignore_bleeding: bool = False) -> Tuple[bool, bool]: client = self.client # Code here assumes successful area change, so it will be sending client notifications @@ -217,11 +220,11 @@ def notify_me(self, area: AreaManager.Area, old_dname: str, area.bleeds_to.add(old_area.name) client.send_ooc('You are bleeding.') - found_something = self.notify_me_rp(area) - return found_something + found_something, ding_something = self.notify_me_rp(area) + return found_something, ding_something def notify_me_rp(self, area: AreaManager.Area, changed_visibility: bool = True, - changed_hearing: bool = True) -> bool: + changed_hearing: bool = True) -> Tuple[bool, bool]: ########### # Check bleeding status blood = self.notify_me_blood(area, changed_visibility=changed_visibility, @@ -238,7 +241,7 @@ def notify_me_rp(self, area: AreaManager.Area, changed_visibility: bool = True, changed_visibility=changed_visibility, changed_hearing=changed_hearing) - return blood or statuses or area_noteworthy + return blood or statuses or area_noteworthy, area_noteworthy def notify_me_blood(self, area: AreaManager.Area, changed_visibility: bool = True, changed_hearing: bool = True) -> bool: @@ -485,7 +488,7 @@ def notify_others(self, area: AreaManager.Area, old_dname: str, status_refreshed_clients = list() # Do not IC ping if client has no status area.broadcast_ic_attention(cond=lambda c: (ic_attention_others or - c in status_refreshed_clients)) + c in status_refreshed_clients), ding=False) def notify_others_moving(self, client: ClientManager.Client, area: AreaManager.Area, autopass_mes: str, blind_mes: str): @@ -777,9 +780,9 @@ def change_area(self, area: AreaManager.Area, override_all: bool = False, # .format(client.get_char_name(), old_area.name, old_area.id, # old_area.name, old_area.id), client) - found_something = client.notify_change_area(area, old_dname, - ignore_bleeding=ignore_bleeding, - ignore_autopass=ignore_autopass) + found_something, ding_something = client.notify_change_area( + area, old_dname, ignore_bleeding=ignore_bleeding, + ignore_autopass=ignore_autopass) old_area.publisher.publish('area_client_left', { 'client': client, @@ -799,7 +802,9 @@ def change_area(self, area: AreaManager.Area, override_all: bool = False, client.area = area client.new_area = area # Update again, as the above if may not have run area.new_client(client) - self.post_area_changed(old_area, area, found_something=found_something, + self.post_area_changed(old_area, area, + found_something=found_something, + ding_something=ding_something, old_dname=old_dname, override_all=override_all, override_passages=override_passages, override_effects=override_effects, @@ -813,7 +818,9 @@ def change_area(self, area: AreaManager.Area, override_all: bool = False, from_party=from_party) def post_area_changed(self, old_area: Union[None, AreaManager.Area], area: AreaManager.Area, - found_something: bool = False, old_dname: str = '', + found_something: bool = False, + ding_something: bool = False, + old_dname: str = '', override_all: bool = False, override_passages: bool = False, override_effects: bool = False, ignore_bleeding: bool = False, ignore_followers: bool = False, @@ -854,7 +861,7 @@ def post_area_changed(self, old_area: Union[None, AreaManager.Area], area: AreaM client.send_ic_blankpost() if found_something: - client.send_ic_attention() + client.send_ic_attention(ding=ding_something) client.reload_music_list() # Update music list to include new area's reachable areas # If new area has lurk callout timer, reset it to that, provided it makes sense diff --git a/server/client_manager.py b/server/client_manager.py index 9900b9343..b11359045 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -19,7 +19,7 @@ from __future__ import annotations import typing -from typing import Any, Callable, List, Optional, Set, Tuple, Dict +from typing import Any, Callable, List, Optional, Set, Tuple, Dict, Union if typing.TYPE_CHECKING: # Avoid circular referencing from server.area_manager import AreaManager @@ -596,8 +596,9 @@ def send_ic_others(self, showname=showname, hide_character=hide_character) - def send_ic_attention(self): - self.send_ic(msg='(Something catches your attention)', ding=1, hide_character=1) + def send_ic_attention(self, ding: bool = True): + int_ding = 1 if ding else 0 + self.send_ic(msg='(Something catches your attention)', ding=int_ding, hide_character=1) def send_ic_blankpost(self): if self.packet_handler.ALLOWS_INVISIBLE_BLANKPOSTS: @@ -951,7 +952,7 @@ def check_change_area(self, area: AreaManager.Area, def notify_change_area(self, area: AreaManager.Area, old_char: str, ignore_bleeding: bool = False, ignore_autopass: bool = False, - just_me: bool = False) -> bool: + just_me: bool = False) -> Tuple[bool, bool]: return self.area_changer.notify_change_area( area, old_char, ignore_bleeding=ignore_bleeding, ignore_autopass=ignore_autopass, just_me=just_me) @@ -981,16 +982,20 @@ def change_area(self, area: AreaManager.Area, override_all: bool = False, more_unavail_chars=more_unavail_chars, from_party=from_party) def post_area_changed(self, old_area: Union[None, AreaManager.Area], area: AreaManager.Area, - found_something: bool = False, old_dname: str = '', - override_all: bool = False, - override_passages: bool = False, override_effects: bool = False, - ignore_bleeding: bool = False, ignore_followers: bool = False, - ignore_autopass: bool = False, - ignore_checks: bool = False, ignore_notifications: bool = False, - more_unavail_chars: Set[int] = None, change_to: int = None, - from_party: bool = False): + found_something: bool = False, + ding_something: bool = False, + old_dname: str = '', + override_all: bool = False, + override_passages: bool = False, override_effects: bool = False, + ignore_bleeding: bool = False, ignore_followers: bool = False, + ignore_autopass: bool = False, + ignore_checks: bool = False, ignore_notifications: bool = False, + more_unavail_chars: Set[int] = None, change_to: int = None, + from_party: bool = False): self.area_changer.post_area_changed( - old_area, area, found_something=found_something, + old_area, area, + found_something=found_something, + ding_something=ding_something, old_dname=old_dname, override_all=override_all, override_passages=override_passages, override_effects=override_effects, @@ -1013,22 +1018,21 @@ def change_blindness(self, blind: bool): self.send_background(name=self.area.background, tod_backgrounds=self.area.get_background_tod()) - found_something = self.area_changer.notify_me_rp(self.area, changed_visibility=changed, - changed_hearing=False) + found_something, ding_something = self.area_changer.notify_me_rp( + self.area, changed_visibility=changed, changed_hearing=False) if found_something and not blind: - self.send_ic_attention() + self.send_ic_attention(ding=ding_something) def change_deafened(self, deaf: bool): changed = (self.is_deaf != deaf) self.is_deaf = deaf - found_something = self.area_changer.notify_me_rp(self.area, changed_visibility=False, - changed_hearing=changed) + found_something, ding_something = self.area_changer.notify_me_rp( + self.area, changed_visibility=False, changed_hearing=changed) if found_something and not deaf: - self.send_ic_attention() + self.send_ic_attention(ding=ding_something) - def change_gagged(self, gagged: bool ): - # changed = (self.is_gagged != gagged) + def change_gagged(self, gagged: bool): self.is_gagged = gagged def check_change_showname(self, showname: str, target_area: AreaManager.Area = None): diff --git a/server/commands.py b/server/commands.py index fd2a70f55..1b6a0fc7c 100644 --- a/server/commands.py +++ b/server/commands.py @@ -4811,7 +4811,7 @@ def ooc_cmd_noteworthy(client: ClientManager.Client, arg: str): if client.area.noteworthy: client.send_ooc_others('Something catches your attention.', is_zstaff_flex=False, in_area=True, pred=lambda c: not (c.is_deaf and c.is_blind)) - client.area.broadcast_ic_attention() + client.area.broadcast_ic_attention(ding=True) logger.log_server('[{}][{}]Set noteworthy status to {}' .format(client.area.id, client.get_char_name(), client.area.noteworthy), @@ -7927,7 +7927,7 @@ def ooc_cmd_status_set(client: ClientManager.Client, arg: str): c.send_ooc(f'You note something different about {client.displayname}.', is_zstaff_flex=False) - client.area.broadcast_ic_attention(cond=lambda c: c in refreshed_clients) + client.area.broadcast_ic_attention(cond=lambda c: c in refreshed_clients, ding=False) else: client.status = '' @@ -7990,7 +7990,7 @@ def ooc_cmd_status_set_other(client: ClientManager.Client, arg: str): c.send_ooc(f'You now note something about {target.displayname}.', is_zstaff_flex=False) - target.area.broadcast_ic_attention() + target.area.broadcast_ic_attention(ding=False) else: # By previous if, player must have had a status before diff --git a/server/tsuserver.py b/server/tsuserver.py index 20cd2ae28..8edf936fd 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -69,8 +69,8 @@ def __init__(self, protocol: AOProtocol = None, self.release = 4 self.major_version = 3 self.minor_version = 4 - self.segment_version = 'b1' - self.internal_version = 'm220820e' + self.segment_version = '' + self.internal_version = '220821a' version_string = self.get_version_string() self.software = 'TsuserverDR {}'.format(version_string) self.version = 'TsuserverDR {} ({})'.format(version_string, self.internal_version)