@@ -45,10 +45,21 @@ def get_ssl_context():
45
45
46
46
47
47
class ClientCommandProcessor (CommandProcessor ):
48
+ """
49
+ The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called
50
+ when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit".
51
+
52
+ The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first
53
+ space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw
54
+ and method("one", "two", "three") without.
55
+
56
+ In addition all docstrings for command methods will be displayed to the user on launch and when using "/help"
57
+ """
48
58
def __init__ (self , ctx : CommonContext ):
49
59
self .ctx = ctx
50
60
51
61
def output (self , text : str ):
62
+ """Helper function to abstract logging to the CommonClient UI"""
52
63
logger .info (text )
53
64
54
65
def _cmd_exit (self ) -> bool :
@@ -164,13 +175,14 @@ def _cmd_ready(self):
164
175
async_start (self .ctx .send_msgs ([{"cmd" : "StatusUpdate" , "status" : state }]), name = "send StatusUpdate" )
165
176
166
177
def default (self , raw : str ):
178
+ """The default message parser to be used when parsing any messages that do not match a command"""
167
179
raw = self .ctx .on_user_say (raw )
168
180
if raw :
169
181
async_start (self .ctx .send_msgs ([{"cmd" : "Say" , "text" : raw }]), name = "send Say" )
170
182
171
183
172
184
class CommonContext :
173
- # Should be adjusted as needed in subclasses
185
+ # The following attributes are used to Connect and should be adjusted as needed in subclasses
174
186
tags : typing .Set [str ] = {"AP" }
175
187
game : typing .Optional [str ] = None
176
188
items_handling : typing .Optional [int ] = None
@@ -343,6 +355,8 @@ def __init__(self, server_address: typing.Optional[str] = None, password: typing
343
355
344
356
self .item_names = self .NameLookupDict (self , "item" )
345
357
self .location_names = self .NameLookupDict (self , "location" )
358
+ self .versions = {}
359
+ self .checksums = {}
346
360
347
361
self .jsontotextparser = JSONtoTextParser (self )
348
362
self .rawjsontotextparser = RawJSONtoTextParser (self )
@@ -429,7 +443,10 @@ async def get_username(self):
429
443
self .auth = await self .console_input ()
430
444
431
445
async def send_connect (self , ** kwargs : typing .Any ) -> None :
432
- """ send `Connect` packet to log in to server """
446
+ """
447
+ Send a `Connect` packet to log in to the server,
448
+ additional keyword args can override any value in the connection packet
449
+ """
433
450
payload = {
434
451
'cmd' : 'Connect' ,
435
452
'password' : self .password , 'name' : self .auth , 'version' : Utils .version_tuple ,
@@ -439,6 +456,7 @@ async def send_connect(self, **kwargs: typing.Any) -> None:
439
456
if kwargs :
440
457
payload .update (kwargs )
441
458
await self .send_msgs ([payload ])
459
+ await self .send_msgs ([{"cmd" : "Get" , "keys" : ["_read_race_mode" ]}])
442
460
443
461
async def console_input (self ) -> str :
444
462
if self .ui :
@@ -459,13 +477,15 @@ def cancel_autoreconnect(self) -> bool:
459
477
return False
460
478
461
479
def slot_concerns_self (self , slot ) -> bool :
480
+ """Helper function to abstract player groups, should be used instead of checking slot == self.slot directly."""
462
481
if slot == self .slot :
463
482
return True
464
483
if slot in self .slot_info :
465
484
return self .slot in self .slot_info [slot ].group_members
466
485
return False
467
486
468
487
def is_echoed_chat (self , print_json_packet : dict ) -> bool :
488
+ """Helper function for filtering out messages sent by self."""
469
489
return print_json_packet .get ("type" , "" ) == "Chat" \
470
490
and print_json_packet .get ("team" , None ) == self .team \
471
491
and print_json_packet .get ("slot" , None ) == self .slot
@@ -497,13 +517,14 @@ def on_user_say(self, text: str) -> typing.Optional[str]:
497
517
"""Gets called before sending a Say to the server from the user.
498
518
Returned text is sent, or sending is aborted if None is returned."""
499
519
return text
500
-
520
+
501
521
def on_ui_command (self , text : str ) -> None :
502
522
"""Gets called by kivy when the user executes a command starting with `/` or `!`.
503
523
The command processor is still called; this is just intended for command echoing."""
504
524
self .ui .print_json ([{"text" : text , "type" : "color" , "color" : "orange" }])
505
525
506
526
def update_permissions (self , permissions : typing .Dict [str , int ]):
527
+ """Internal method to parse and save server permissions from RoomInfo"""
507
528
for permission_name , permission_flag in permissions .items ():
508
529
try :
509
530
flag = Permission (permission_flag )
@@ -552,26 +573,34 @@ async def prepare_data_package(self, relevant_games: typing.Set[str],
552
573
needed_updates .add (game )
553
574
continue
554
575
555
- local_version : int = network_data_package ["games" ].get (game , {}).get ("version" , 0 )
556
- local_checksum : typing .Optional [str ] = network_data_package ["games" ].get (game , {}).get ("checksum" )
557
- # no action required if local version is new enough
558
- if (not remote_checksum and (remote_version > local_version or remote_version == 0 )) \
559
- or remote_checksum != local_checksum :
560
- cached_game = Utils .load_data_package_for_checksum (game , remote_checksum )
561
- cache_version : int = cached_game .get ("version" , 0 )
562
- cache_checksum : typing .Optional [str ] = cached_game .get ("checksum" )
563
- # download remote version if cache is not new enough
564
- if (not remote_checksum and (remote_version > cache_version or remote_version == 0 )) \
565
- or remote_checksum != cache_checksum :
566
- needed_updates .add (game )
576
+ cached_version : int = self .versions .get (game , 0 )
577
+ cached_checksum : typing .Optional [str ] = self .checksums .get (game )
578
+ # no action required if cached version is new enough
579
+ if (not remote_checksum and (remote_version > cached_version or remote_version == 0 )) \
580
+ or remote_checksum != cached_checksum :
581
+ local_version : int = network_data_package ["games" ].get (game , {}).get ("version" , 0 )
582
+ local_checksum : typing .Optional [str ] = network_data_package ["games" ].get (game , {}).get ("checksum" )
583
+ if ((remote_checksum or remote_version <= local_version and remote_version != 0 )
584
+ and remote_checksum == local_checksum ):
585
+ self .update_game (network_data_package ["games" ][game ], game )
567
586
else :
568
- self .update_game (cached_game , game )
587
+ cached_game = Utils .load_data_package_for_checksum (game , remote_checksum )
588
+ cache_version : int = cached_game .get ("version" , 0 )
589
+ cache_checksum : typing .Optional [str ] = cached_game .get ("checksum" )
590
+ # download remote version if cache is not new enough
591
+ if (not remote_checksum and (remote_version > cache_version or remote_version == 0 )) \
592
+ or remote_checksum != cache_checksum :
593
+ needed_updates .add (game )
594
+ else :
595
+ self .update_game (cached_game , game )
569
596
if needed_updates :
570
597
await self .send_msgs ([{"cmd" : "GetDataPackage" , "games" : [game_name ]} for game_name in needed_updates ])
571
598
572
599
def update_game (self , game_package : dict , game : str ):
573
600
self .item_names .update_game (game , game_package ["item_name_to_id" ])
574
601
self .location_names .update_game (game , game_package ["location_name_to_id" ])
602
+ self .versions [game ] = game_package .get ("version" , 0 )
603
+ self .checksums [game ] = game_package .get ("checksum" )
575
604
576
605
def update_data_package (self , data_package : dict ):
577
606
for game , game_data in data_package ["games" ].items ():
@@ -613,6 +642,7 @@ def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
613
642
logger .info (f"DeathLink: Received from { data ['source' ]} " )
614
643
615
644
async def send_death (self , death_text : str = "" ):
645
+ """Helper function to send a deathlink using death_text as the unique death cause string."""
616
646
if self .server and self .server .socket :
617
647
logger .info ("DeathLink: Sending death to your friends..." )
618
648
self .last_death_link = time .time ()
@@ -626,6 +656,7 @@ async def send_death(self, death_text: str = ""):
626
656
}])
627
657
628
658
async def update_death_link (self , death_link : bool ):
659
+ """Helper function to set Death Link connection tag on/off and update the connection if already connected."""
629
660
old_tags = self .tags .copy ()
630
661
if death_link :
631
662
self .tags .add ("DeathLink" )
@@ -635,7 +666,7 @@ async def update_death_link(self, death_link: bool):
635
666
await self .send_msgs ([{"cmd" : "ConnectUpdate" , "tags" : self .tags }])
636
667
637
668
def gui_error (self , title : str , text : typing .Union [Exception , str ]) -> typing .Optional ["kvui.MessageBox" ]:
638
- """Displays an error messagebox"""
669
+ """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework """
639
670
if not self .ui :
640
671
return None
641
672
title = title or "Error"
@@ -987,6 +1018,7 @@ async def console_loop(ctx: CommonContext):
987
1018
988
1019
989
1020
def get_base_parser (description : typing .Optional [str ] = None ):
1021
+ """Base argument parser to be reused for components subclassing off of CommonClient"""
990
1022
import argparse
991
1023
parser = argparse .ArgumentParser (description = description )
992
1024
parser .add_argument ('--connect' , default = None , help = 'Address of the multiworld host.' )
@@ -1037,6 +1069,7 @@ async def main(args):
1037
1069
parser .add_argument ("url" , nargs = "?" , help = "Archipelago connection url" )
1038
1070
args = parser .parse_args (args )
1039
1071
1072
+ # handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
1040
1073
if args .url :
1041
1074
url = urllib .parse .urlparse (args .url )
1042
1075
if url .scheme == "archipelago" :
@@ -1048,6 +1081,7 @@ async def main(args):
1048
1081
else :
1049
1082
parser .error (f"bad url, found { args .url } , expected url in form of archipelago://archipelago.gg:38281" )
1050
1083
1084
+ # use colorama to display colored text highlighting on windows
1051
1085
colorama .init ()
1052
1086
1053
1087
asyncio .run (main (args ))
0 commit comments