3232
3333import colorama
3434import websockets
35- from websockets .extensions .permessage_deflate import PerMessageDeflate
35+ from websockets .extensions .permessage_deflate import PerMessageDeflate , ServerPerMessageDeflateFactory
3636try :
3737 # ponyorm is a requirement for webhost, not default server, so may not be importable
3838 from pony .orm .dbapiprovider import OperationalError
5050min_client_version = Version (0 , 5 , 0 )
5151colorama .just_fix_windows_console ()
5252
53+ no_version = Version (0 , 0 , 0 )
54+ assert isinstance (no_version , tuple ) # assert immutable
55+
56+ server_per_message_deflate_factory = ServerPerMessageDeflateFactory (
57+ server_max_window_bits = 11 ,
58+ client_max_window_bits = 11 ,
59+ compress_settings = {"memLevel" : 4 },
60+ )
61+
5362
5463def remove_from_list (container , value ):
5564 try :
@@ -125,8 +134,31 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
125134
126135
127136class Client (Endpoint ):
128- version = Version (0 , 0 , 0 )
129- tags : typing .List [str ]
137+ __slots__ = (
138+ "__weakref__" ,
139+ "version" ,
140+ "auth" ,
141+ "team" ,
142+ "slot" ,
143+ "send_index" ,
144+ "tags" ,
145+ "messageprocessor" ,
146+ "ctx" ,
147+ "remote_items" ,
148+ "remote_start_inventory" ,
149+ "no_items" ,
150+ "no_locations" ,
151+ "no_text" ,
152+ )
153+
154+ version : Version
155+ auth : bool
156+ team : int | None
157+ slot : int | None
158+ send_index : int
159+ tags : list [str ]
160+ messageprocessor : ClientMessageProcessor
161+ ctx : weakref .ref [Context ]
130162 remote_items : bool
131163 remote_start_inventory : bool
132164 no_items : bool
@@ -135,13 +167,19 @@ class Client(Endpoint):
135167
136168 def __init__ (self , socket : "ServerConnection" , ctx : Context ) -> None :
137169 super ().__init__ (socket )
170+ self .version = no_version
138171 self .auth = False
139172 self .team = None
140173 self .slot = None
141174 self .send_index = 0
142175 self .tags = []
143176 self .messageprocessor = client_message_processor (ctx , self )
144177 self .ctx = weakref .ref (ctx )
178+ self .remote_items = False
179+ self .remote_start_inventory = False
180+ self .no_items = False
181+ self .no_locations = False
182+ self .no_text = False
145183
146184 @property
147185 def items_handling (self ):
@@ -179,6 +217,7 @@ class Context:
179217 "release_mode" : str ,
180218 "remaining_mode" : str ,
181219 "collect_mode" : str ,
220+ "countdown_mode" : str ,
182221 "item_cheat" : bool ,
183222 "compatibility" : int }
184223 # team -> slot id -> list of clients authenticated to slot.
@@ -208,8 +247,8 @@ class Context:
208247
209248 def __init__ (self , host : str , port : int , server_password : str , password : str , location_check_points : int ,
210249 hint_cost : int , item_cheat : bool , release_mode : str = "disabled" , collect_mode = "disabled" ,
211- remaining_mode : str = "disabled" , auto_shutdown : typing .SupportsFloat = 0 , compatibility : int = 2 ,
212- log_network : bool = False , logger : logging .Logger = logging .getLogger ()):
250+ countdown_mode : str = "auto" , remaining_mode : str = "disabled" , auto_shutdown : typing .SupportsFloat = 0 ,
251+ compatibility : int = 2 , log_network : bool = False , logger : logging .Logger = logging .getLogger ()):
213252 self .logger = logger
214253 super (Context , self ).__init__ ()
215254 self .slot_info = {}
@@ -242,6 +281,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo
242281 self .release_mode : str = release_mode
243282 self .remaining_mode : str = remaining_mode
244283 self .collect_mode : str = collect_mode
284+ self .countdown_mode : str = countdown_mode
245285 self .item_cheat = item_cheat
246286 self .exit_event = asyncio .Event ()
247287 self .client_activity_timers : typing .Dict [
@@ -627,6 +667,7 @@ def get_save(self) -> dict:
627667 "server_password" : self .server_password , "password" : self .password ,
628668 "release_mode" : self .release_mode ,
629669 "remaining_mode" : self .remaining_mode , "collect_mode" : self .collect_mode ,
670+ "countdown_mode" : self .countdown_mode ,
630671 "item_cheat" : self .item_cheat , "compatibility" : self .compatibility }
631672
632673 }
@@ -661,6 +702,7 @@ def set_save(self, savedata: dict):
661702 self .release_mode = savedata ["game_options" ]["release_mode" ]
662703 self .remaining_mode = savedata ["game_options" ]["remaining_mode" ]
663704 self .collect_mode = savedata ["game_options" ]["collect_mode" ]
705+ self .countdown_mode = savedata ["game_options" ].get ("countdown_mode" , self .countdown_mode )
664706 self .item_cheat = savedata ["game_options" ]["item_cheat" ]
665707 self .compatibility = savedata ["game_options" ]["compatibility" ]
666708
@@ -1492,6 +1534,23 @@ def _cmd_collect(self) -> bool:
14921534 " You can ask the server admin for a /collect" )
14931535 return False
14941536
1537+ def _cmd_countdown (self , seconds : str = "10" ) -> bool :
1538+ """Start a countdown in seconds"""
1539+ if self .ctx .countdown_mode == "disabled" or \
1540+ self .ctx .countdown_mode == "auto" and len (self .ctx .player_names ) >= 30 :
1541+ self .output ("Sorry, client countdowns have been disabled on this server. You can ask the server admin for a /countdown" )
1542+ return False
1543+ try :
1544+ timer = int (seconds , 10 )
1545+ except ValueError :
1546+ timer = 10
1547+ else :
1548+ if timer > 60 * 60 :
1549+ raise ValueError (f"{ timer } is invalid. Maximum is 1 hour." )
1550+
1551+ async_start (countdown (self .ctx , timer ))
1552+ return True
1553+
14951554 def _cmd_remaining (self ) -> bool :
14961555 """List remaining items in your game, but not their location or recipient"""
14971556 if self .ctx .remaining_mode == "enabled" :
@@ -2452,6 +2511,11 @@ def value_type(input_text: str):
24522511 elif value_type == str and option_name .endswith ("password" ):
24532512 def value_type (input_text : str ):
24542513 return None if input_text .lower () in {"null" , "none" , '""' , "''" } else input_text
2514+ elif option_name == "countdown_mode" :
2515+ valid_values = {"enabled" , "disabled" , "auto" }
2516+ if option_value .lower () not in valid_values :
2517+ self .output (f"Unrecognized { option_name } value '{ option_value } ', known: { ', ' .join (valid_values )} " )
2518+ return False
24552519 elif value_type == str and option_name .endswith ("mode" ):
24562520 valid_values = {"goal" , "enabled" , "disabled" }
24572521 valid_values .update (("auto" , "auto_enabled" ) if option_name != "remaining_mode" else [])
@@ -2539,6 +2603,13 @@ def parse_args() -> argparse.Namespace:
25392603 goal: !collect can be used after goal completion
25402604 auto-enabled: !collect is available and automatically triggered on goal completion
25412605 ''' )
2606+ parser .add_argument ('--countdown_mode' , default = defaults ["countdown_mode" ], nargs = '?' ,
2607+ choices = ['enabled' , 'disabled' , "auto" ], help = '''\
2608+ Select !countdown Accessibility. (default: %(default)s)
2609+ enabled: !countdown is always available
2610+ disabled: !countdown is never available
2611+ auto: !countdown is available for rooms with less than 30 players
2612+ ''' )
25422613 parser .add_argument ('--remaining_mode' , default = defaults ["remaining_mode" ], nargs = '?' ,
25432614 choices = ['enabled' , 'disabled' , "goal" ], help = '''\
25442615 Select !remaining Accessibility. (default: %(default)s)
@@ -2604,7 +2675,7 @@ async def main(args: argparse.Namespace):
26042675
26052676 ctx = Context (args .host , args .port , args .server_password , args .password , args .location_check_points ,
26062677 args .hint_cost , not args .disable_item_cheat , args .release_mode , args .collect_mode ,
2607- args .remaining_mode ,
2678+ args .countdown_mode , args . remaining_mode ,
26082679 args .auto_shutdown , args .compatibility , args .log_network )
26092680 data_filename = args .multidata
26102681
@@ -2639,7 +2710,13 @@ async def main(args: argparse.Namespace):
26392710
26402711 ssl_context = load_server_cert (args .cert , args .cert_key ) if args .cert else None
26412712
2642- ctx .server = websockets .serve (functools .partial (server , ctx = ctx ), host = ctx .host , port = ctx .port , ssl = ssl_context )
2713+ ctx .server = websockets .serve (
2714+ functools .partial (server , ctx = ctx ),
2715+ host = ctx .host ,
2716+ port = ctx .port ,
2717+ ssl = ssl_context ,
2718+ extensions = [server_per_message_deflate_factory ],
2719+ )
26432720 ip = args .host if args .host else Utils .get_public_ipv4 ()
26442721 logging .info ('Hosting game at %s:%d (%s)' % (ip , ctx .port ,
26452722 'No password' if not ctx .password else 'Password: %s' % ctx .password ))
0 commit comments