| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | from __future__ import annotations | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import asyncio | 
					
						
							|  |  |  | import urllib.parse | 
					
						
							| 
									
										
										
										
											2021-08-20 22:31:17 +02:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  | import typing | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | import time | 
					
						
							| 
									
										
										
										
											2022-09-09 21:28:24 +02:00
										 |  |  | import functools | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:19:18 +02:00
										 |  |  | import ModuleUpdate | 
					
						
							|  |  |  | ModuleUpdate.update() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | import websockets | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Utils | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							| 
									
										
										
										
											2021-11-17 22:46:32 +01:00
										 |  |  |     Utils.init_logging("TextClient", exception_logger="Client") | 
					
						
							| 
									
										
										
										
											2021-11-10 15:35:43 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | from MultiServer import CommandProcessor | 
					
						
							| 
									
										
										
										
											2022-09-09 21:28:24 +02:00
										 |  |  | from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \ | 
					
						
							|  |  |  |     ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  | from Utils import Version, stream_input, async_start | 
					
						
							| 
									
										
										
										
											2021-07-12 20:07:02 +02:00
										 |  |  | from worlds import network_data_package, AutoWorldRegister | 
					
						
							| 
									
										
										
										
											2022-04-06 00:06:48 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2023-06-22 00:01:41 +02:00
										 |  |  | import ssl | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  | if typing.TYPE_CHECKING: | 
					
						
							|  |  |  |     import kvui | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | logger = logging.getLogger("Client") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-21 22:24:49 +02:00
										 |  |  | # without terminal, we have to use gui mode | 
					
						
							| 
									
										
										
										
											2021-11-09 12:53:05 +01:00
										 |  |  | gui_enabled = not sys.stdout or "--nogui" not in sys.argv | 
					
						
							| 
									
										
										
										
											2021-08-04 18:38:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 00:01:41 +02:00
										 |  |  | @Utils.cache_argsless | 
					
						
							|  |  |  | def get_ssl_context(): | 
					
						
							|  |  |  |     import certifi | 
					
						
							|  |  |  |     return ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | class ClientCommandProcessor(CommandProcessor): | 
					
						
							|  |  |  |     def __init__(self, ctx: CommonContext): | 
					
						
							|  |  |  |         self.ctx = ctx | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def output(self, text: str): | 
					
						
							|  |  |  |         logger.info(text) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_exit(self) -> bool: | 
					
						
							|  |  |  |         """Close connections and client""" | 
					
						
							|  |  |  |         self.ctx.exit_event.set() | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_connect(self, address: str = "") -> bool: | 
					
						
							|  |  |  |         """Connect to a MultiWorld Server""" | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         if address: | 
					
						
							|  |  |  |             self.ctx.server_address = None | 
					
						
							|  |  |  |             self.ctx.username = None | 
					
						
							|  |  |  |         elif not self.ctx.server_address: | 
					
						
							|  |  |  |             self.output("Please specify an address.") | 
					
						
							|  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(self.ctx.connect(address if address else None), name="connecting") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_disconnect(self) -> bool: | 
					
						
							|  |  |  |         """Disconnect from a MultiWorld Server""" | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(self.ctx.disconnect(), name="disconnecting") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_received(self) -> bool: | 
					
						
							|  |  |  |         """List all received items""" | 
					
						
							| 
									
										
										
										
											2023-03-04 16:34:10 +01:00
										 |  |  |         self.output(f'{len(self.ctx.items_received)} received items:') | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         for index, item in enumerate(self.ctx.items_received, 1): | 
					
						
							| 
									
										
										
										
											2022-06-21 15:46:35 +02:00
										 |  |  |             self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |     def _cmd_missing(self, filter_text = "") -> bool: | 
					
						
							|  |  |  |         """List all missing location checks, from your local game state.
 | 
					
						
							|  |  |  |         Can be given text, which will be used as filter."""
 | 
					
						
							| 
									
										
										
										
											2021-10-30 07:33:05 +02:00
										 |  |  |         if not self.ctx.game: | 
					
						
							|  |  |  |             self.output("No game set, cannot determine missing checks.") | 
					
						
							| 
									
										
										
										
											2021-11-09 12:53:05 +01:00
										 |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         count = 0 | 
					
						
							|  |  |  |         checked_count = 0 | 
					
						
							| 
									
										
										
										
											2021-08-10 04:38:29 +02:00
										 |  |  |         for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items(): | 
					
						
							| 
									
										
										
										
											2023-03-08 17:53:43 +01:00
										 |  |  |             if filter_text and filter_text not in location: | 
					
						
							|  |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             if location_id < 0: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if location_id not in self.ctx.locations_checked: | 
					
						
							|  |  |  |                 if location_id in self.ctx.missing_locations: | 
					
						
							|  |  |  |                     self.output('Missing: ' + location) | 
					
						
							|  |  |  |                     count += 1 | 
					
						
							|  |  |  |                 elif location_id in self.ctx.checked_locations: | 
					
						
							|  |  |  |                     self.output('Checked: ' + location) | 
					
						
							|  |  |  |                     count += 1 | 
					
						
							|  |  |  |                     checked_count += 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if count: | 
					
						
							|  |  |  |             self.output( | 
					
						
							|  |  |  |                 f"Found {count} missing location checks{f'. {checked_count} location checks previously visited.' if checked_count else ''}") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.output("No missing location checks found.") | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-23 21:47:23 +01:00
										 |  |  |     def _cmd_items(self): | 
					
						
							| 
									
										
										
										
											2021-12-03 05:14:44 +01:00
										 |  |  |         """List all item names for the currently running game.""" | 
					
						
							| 
									
										
										
										
											2022-10-25 13:54:43 -04:00
										 |  |  |         if not self.ctx.game: | 
					
						
							|  |  |  |             self.output("No game set, cannot determine existing items.") | 
					
						
							|  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-11-23 21:47:23 +01:00
										 |  |  |         self.output(f"Item Names for {self.ctx.game}") | 
					
						
							|  |  |  |         for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id: | 
					
						
							|  |  |  |             self.output(item_name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_locations(self): | 
					
						
							| 
									
										
										
										
											2021-12-03 05:14:44 +01:00
										 |  |  |         """List all location names for the currently running game.""" | 
					
						
							| 
									
										
										
										
											2022-10-25 13:54:43 -04:00
										 |  |  |         if not self.ctx.game: | 
					
						
							|  |  |  |             self.output("No game set, cannot determine existing locations.") | 
					
						
							|  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-11-23 21:47:23 +01:00
										 |  |  |         self.output(f"Location Names for {self.ctx.game}") | 
					
						
							|  |  |  |         for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id: | 
					
						
							|  |  |  |             self.output(location_name) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     def _cmd_ready(self): | 
					
						
							| 
									
										
										
										
											2021-12-03 07:04:17 +01:00
										 |  |  |         """Send ready status to server.""" | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.ctx.ready = not self.ctx.ready | 
					
						
							|  |  |  |         if self.ctx.ready: | 
					
						
							|  |  |  |             state = ClientStatus.CLIENT_READY | 
					
						
							|  |  |  |             self.output("Readied up.") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             state = ClientStatus.CLIENT_CONNECTED | 
					
						
							|  |  |  |             self.output("Unreadied.") | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |         async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def default(self, raw: str): | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |         raw = self.ctx.on_user_say(raw) | 
					
						
							|  |  |  |         if raw: | 
					
						
							| 
									
										
										
										
											2022-11-02 07:51:35 -07:00
										 |  |  |             async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-04 18:38:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  | class CommonContext: | 
					
						
							|  |  |  |     # Should be adjusted as needed in subclasses | 
					
						
							| 
									
										
										
										
											2021-10-29 10:03:15 +02:00
										 |  |  |     tags: typing.Set[str] = {"AP"} | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     game: typing.Optional[str] = None | 
					
						
							|  |  |  |     items_handling: typing.Optional[int] = None | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |     want_slot_data: bool = True  # should slot_data be retrieved via Connect | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     # data package | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |     # Contents in flux until connection to server is made, to download correct data for this multiworld. | 
					
						
							|  |  |  |     item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') | 
					
						
							|  |  |  |     location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     # defaults | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  |     starting_reconnect_delay: int = 5 | 
					
						
							|  |  |  |     current_reconnect_delay: int = starting_reconnect_delay | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  |     ui = None | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     ui_task: typing.Optional["asyncio.Task[None]"] = None | 
					
						
							|  |  |  |     input_task: typing.Optional["asyncio.Task[None]"] = None | 
					
						
							|  |  |  |     keep_alive_task: typing.Optional["asyncio.Task[None]"] = None | 
					
						
							|  |  |  |     server_task: typing.Optional["asyncio.Task[None]"] = None | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None | 
					
						
							|  |  |  |     disconnected_intentionally: bool = False | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     server: typing.Optional[Endpoint] = None | 
					
						
							|  |  |  |     server_version: Version = Version(0, 0, 0) | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |     generator_version: Version = Version(0, 0, 0) | 
					
						
							| 
									
										
										
										
											2022-10-31 03:55:18 +01:00
										 |  |  |     current_energy_link_value: typing.Optional[int] = None  # to display in UI, gets set by server | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     last_death_link: float = time.time()  # last send/received death link on AP layer | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # remaining type info | 
					
						
							|  |  |  |     slot_info: typing.Dict[int, NetworkSlot] | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     server_address: typing.Optional[str] | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     password: typing.Optional[str] | 
					
						
							|  |  |  |     hint_cost: typing.Optional[int] | 
					
						
							| 
									
										
										
										
											2023-04-10 14:44:20 -05:00
										 |  |  |     hint_points: typing.Optional[int] | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     player_names: typing.Dict[int, str] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     finished_game: bool | 
					
						
							|  |  |  |     ready: bool | 
					
						
							|  |  |  |     auth: typing.Optional[str] | 
					
						
							|  |  |  |     seed_name: typing.Optional[str] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     # locations | 
					
						
							|  |  |  |     locations_checked: typing.Set[int]  # local state | 
					
						
							|  |  |  |     locations_scouted: typing.Set[int] | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     items_received: typing.List[NetworkItem] | 
					
						
							| 
									
										
										
										
											2022-08-31 20:55:15 +02:00
										 |  |  |     missing_locations: typing.Set[int]  # server state | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     checked_locations: typing.Set[int]  # server state | 
					
						
							| 
									
										
										
										
											2022-08-31 20:55:15 +02:00
										 |  |  |     server_locations: typing.Set[int]  # all locations the server knows of, missing_location | checked_locations | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     locations_info: typing.Dict[int, NetworkItem] | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-29 15:06:58 +02:00
										 |  |  |     # data storage | 
					
						
							|  |  |  |     stored_data: typing.Dict[str, typing.Any] | 
					
						
							|  |  |  |     stored_data_notification_keys: typing.Set[str] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |     # internals | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |     # current message box through kvui | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     _messagebox: typing.Optional["kvui.MessageBox"] = None | 
					
						
							|  |  |  |     # message box reporting a loss of connection | 
					
						
							|  |  |  |     _messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None | 
					
						
							| 
									
										
										
										
											2021-07-12 20:07:02 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None: | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         # server state | 
					
						
							|  |  |  |         self.server_address = server_address | 
					
						
							| 
									
										
										
										
											2022-06-27 03:10:41 -07:00
										 |  |  |         self.username = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.password = password | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |         self.hint_cost = None | 
					
						
							| 
									
										
										
										
											2022-05-24 00:20:02 +02:00
										 |  |  |         self.slot_info = {} | 
					
						
							| 
									
										
										
										
											2021-10-22 05:25:09 +02:00
										 |  |  |         self.permissions = { | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |             "release": "disabled", | 
					
						
							| 
									
										
										
										
											2021-10-22 05:25:09 +02:00
										 |  |  |             "collect": "disabled", | 
					
						
							|  |  |  |             "remaining": "disabled", | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # own state | 
					
						
							|  |  |  |         self.finished_game = False | 
					
						
							|  |  |  |         self.ready = False | 
					
						
							|  |  |  |         self.team = None | 
					
						
							|  |  |  |         self.slot = None | 
					
						
							|  |  |  |         self.auth = None | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |         self.seed_name = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |         self.locations_checked = set()  # local state | 
					
						
							|  |  |  |         self.locations_scouted = set() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.items_received = [] | 
					
						
							| 
									
										
										
										
											2022-08-31 20:55:15 +02:00
										 |  |  |         self.missing_locations = set()  # server state | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |         self.checked_locations = set()  # server state | 
					
						
							| 
									
										
										
										
											2022-08-31 20:55:15 +02:00
										 |  |  |         self.server_locations = set()  # all locations the server knows of, missing_location | checked_locations | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |         self.locations_info = {} | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-29 15:06:58 +02:00
										 |  |  |         self.stored_data = {} | 
					
						
							|  |  |  |         self.stored_data_notification_keys = set() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.input_queue = asyncio.Queue() | 
					
						
							|  |  |  |         self.input_requests = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # game state | 
					
						
							| 
									
										
										
										
											2022-06-08 00:34:45 +02:00
										 |  |  |         self.player_names = {0: "Archipelago"} | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.exit_event = asyncio.Event() | 
					
						
							|  |  |  |         self.watcher_event = asyncio.Event() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.jsontotextparser = JSONtoTextParser(self) | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         self.update_data_package(network_data_package) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  |         # execution | 
					
						
							| 
									
										
										
										
											2021-11-21 02:02:40 +01:00
										 |  |  |         self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy") | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-01 06:54:40 +01:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def suggested_address(self) -> str: | 
					
						
							|  |  |  |         if self.server_address: | 
					
						
							|  |  |  |             return self.server_address | 
					
						
							|  |  |  |         return Utils.persistent_load().get("client", {}).get("last_server_address", "") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-09 21:28:24 +02:00
										 |  |  |     @functools.cached_property | 
					
						
							|  |  |  |     def raw_text_parser(self) -> RawJSONtoTextParser: | 
					
						
							|  |  |  |         return RawJSONtoTextParser(self) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-29 10:03:15 +02:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def total_locations(self) -> typing.Optional[int]: | 
					
						
							|  |  |  |         """Will return None until connected.""" | 
					
						
							|  |  |  |         if self.checked_locations or self.missing_locations: | 
					
						
							|  |  |  |             return len(self.checked_locations | self.missing_locations) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     async def connection_closed(self): | 
					
						
							| 
									
										
										
										
											2022-05-21 22:24:49 +02:00
										 |  |  |         if self.server and self.server.socket is not None: | 
					
						
							|  |  |  |             await self.server.socket.close() | 
					
						
							| 
									
										
										
										
											2022-11-01 06:54:40 +01:00
										 |  |  |         self.reset_server_state() | 
					
						
							| 
									
										
										
										
											2022-05-21 22:24:49 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def reset_server_state(self): | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.auth = None | 
					
						
							| 
									
										
										
										
											2022-05-24 00:20:02 +02:00
										 |  |  |         self.slot = None | 
					
						
							|  |  |  |         self.team = None | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.items_received = [] | 
					
						
							|  |  |  |         self.locations_info = {} | 
					
						
							|  |  |  |         self.server_version = Version(0, 0, 0) | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |         self.generator_version = Version(0, 0, 0) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.server = None | 
					
						
							|  |  |  |         self.server_task = None | 
					
						
							| 
									
										
										
										
											2022-05-21 22:24:49 +02:00
										 |  |  |         self.hint_cost = None | 
					
						
							|  |  |  |         self.permissions = { | 
					
						
							| 
									
										
										
										
											2023-01-24 03:36:27 +01:00
										 |  |  |             "release": "disabled", | 
					
						
							| 
									
										
										
										
											2022-05-21 22:24:49 +02:00
										 |  |  |             "collect": "disabled", | 
					
						
							|  |  |  |             "remaining": "disabled", | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     async def disconnect(self, allow_autoreconnect: bool = False): | 
					
						
							|  |  |  |         if not allow_autoreconnect: | 
					
						
							|  |  |  |             self.disconnected_intentionally = True | 
					
						
							|  |  |  |             if self.cancel_autoreconnect(): | 
					
						
							|  |  |  |                 logger.info("Cancelled auto-reconnect.") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         if self.server and not self.server.socket.closed: | 
					
						
							|  |  |  |             await self.server.socket.close() | 
					
						
							|  |  |  |         if self.server_task is not None: | 
					
						
							|  |  |  |             await self.server_task | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     async def send_msgs(self, msgs: typing.List[typing.Any]) -> None: | 
					
						
							|  |  |  |         """ `msgs` JSON serializable """ | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         if not self.server or not self.server.socket.open or self.server.socket.closed: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         await self.server.socket.send(encode(msgs)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def consume_players_package(self, package: typing.List[tuple]): | 
					
						
							|  |  |  |         self.player_names = {slot: name for team, slot, name, orig_name in package if self.team == team} | 
					
						
							| 
									
										
										
										
											2021-05-11 23:08:50 +02:00
										 |  |  |         self.player_names[0] = "Archipelago" | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def event_invalid_slot(self): | 
					
						
							|  |  |  |         raise Exception('Invalid Slot; please verify that you have connected to the correct world.') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 04:32:09 +02:00
										 |  |  |     def event_invalid_game(self): | 
					
						
							|  |  |  |         raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |     async def server_auth(self, password_requested: bool = False): | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         if password_requested and not self.password: | 
					
						
							|  |  |  |             logger.info('Enter the password required to join this game:') | 
					
						
							|  |  |  |             self.password = await self.console_input() | 
					
						
							|  |  |  |             return self.password | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 03:10:41 -07:00
										 |  |  |     async def get_username(self): | 
					
						
							|  |  |  |         if not self.auth: | 
					
						
							|  |  |  |             self.auth = self.username | 
					
						
							|  |  |  |             if not self.auth: | 
					
						
							|  |  |  |                 logger.info('Enter slot name:') | 
					
						
							|  |  |  |                 self.auth = await self.console_input() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     async def send_connect(self, **kwargs: typing.Any) -> None: | 
					
						
							| 
									
										
										
										
											2022-10-23 09:18:05 -07:00
										 |  |  |         """ send `Connect` packet to log in to server """ | 
					
						
							| 
									
										
										
										
											2021-11-21 02:50:24 +01:00
										 |  |  |         payload = { | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |             'cmd': 'Connect', | 
					
						
							| 
									
										
										
										
											2021-11-21 02:50:24 +01:00
										 |  |  |             'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, | 
					
						
							| 
									
										
										
										
											2022-01-23 06:38:46 +01:00
										 |  |  |             'tags': self.tags, 'items_handling': self.items_handling, | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |             'uuid': Utils.get_unique_identifier(), 'game': self.game, "slot_data": self.want_slot_data, | 
					
						
							| 
									
										
										
										
											2021-11-21 02:50:24 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |         if kwargs: | 
					
						
							|  |  |  |             payload.update(kwargs) | 
					
						
							|  |  |  |         await self.send_msgs([payload]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     async def console_input(self) -> str: | 
					
						
							| 
									
										
										
										
											2022-11-01 06:54:40 +01:00
										 |  |  |         if self.ui: | 
					
						
							|  |  |  |             self.ui.focus_textinput() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         self.input_requests += 1 | 
					
						
							|  |  |  |         return await self.input_queue.get() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-20 10:41:11 -07:00
										 |  |  |     async def connect(self, address: typing.Optional[str] = None) -> None: | 
					
						
							| 
									
										
										
										
											2022-10-23 09:18:05 -07:00
										 |  |  |         """ disconnect any previous connection, and open new connection to the server """ | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         await self.disconnect() | 
					
						
							| 
									
										
										
										
											2021-11-21 02:02:40 +01:00
										 |  |  |         self.server_task = asyncio.create_task(server_loop(self, address), name="server loop") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     def cancel_autoreconnect(self) -> bool: | 
					
						
							|  |  |  |         if self.autoreconnect_task: | 
					
						
							|  |  |  |             self.autoreconnect_task.cancel() | 
					
						
							|  |  |  |             self.autoreconnect_task = None | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-30 23:02:40 +02:00
										 |  |  |     def slot_concerns_self(self, slot) -> bool: | 
					
						
							|  |  |  |         if slot == self.slot: | 
					
						
							|  |  |  |             return True | 
					
						
							|  |  |  |         if slot in self.slot_info: | 
					
						
							|  |  |  |             return self.slot in self.slot_info[slot].group_members | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-02-13 03:17:25 +01:00
										 |  |  |     def is_echoed_chat(self, print_json_packet: dict) -> bool: | 
					
						
							|  |  |  |         return print_json_packet.get("type", "") == "Chat" \ | 
					
						
							|  |  |  |             and print_json_packet.get("team", None) == self.team \ | 
					
						
							|  |  |  |             and print_json_packet.get("slot", None) == self.slot | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-28 00:07:57 +02:00
										 |  |  |     def is_uninteresting_item_send(self, print_json_packet: dict) -> bool: | 
					
						
							|  |  |  |         """Helper function for filtering out ItemSend prints that do not concern the local player.""" | 
					
						
							|  |  |  |         return print_json_packet.get("type", "") == "ItemSend" \ | 
					
						
							|  |  |  |             and not self.slot_concerns_self(print_json_packet["receiving"]) \ | 
					
						
							|  |  |  |             and not self.slot_concerns_self(print_json_packet["item"].player) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |     def on_print(self, args: dict): | 
					
						
							|  |  |  |         logger.info(args["text"]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def on_print_json(self, args: dict): | 
					
						
							| 
									
										
										
										
											2021-07-31 01:53:06 +02:00
										 |  |  |         if self.ui: | 
					
						
							|  |  |  |             self.ui.print_json(args["data"]) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             text = self.jsontotextparser(args["data"]) | 
					
						
							|  |  |  |             logger.info(text) | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-31 19:45:17 +02:00
										 |  |  |     def on_package(self, cmd: str, args: dict): | 
					
						
							|  |  |  |         """For custom package handling in subclasses.""" | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |     def on_user_say(self, text: str) -> typing.Optional[str]: | 
					
						
							|  |  |  |         """Gets called before sending a Say to the server from the user.
 | 
					
						
							|  |  |  |         Returned text is sent, or sending is aborted if None is returned."""
 | 
					
						
							|  |  |  |         return text | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-22 05:25:09 +02:00
										 |  |  |     def update_permissions(self, permissions: typing.Dict[str, int]): | 
					
						
							|  |  |  |         for permission_name, permission_flag in permissions.items(): | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 flag = Permission(permission_flag) | 
					
						
							|  |  |  |                 logger.info(f"{permission_name.capitalize()} permission: {flag.name}") | 
					
						
							|  |  |  |                 self.permissions[permission_name] = flag.name | 
					
						
							|  |  |  |             except Exception as e:  # safeguard against permissions that may be implemented in the future | 
					
						
							|  |  |  |                 logger.exception(e) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |     async def shutdown(self): | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |         self.server_address = "" | 
					
						
							| 
									
										
										
										
											2022-06-27 03:10:41 -07:00
										 |  |  |         self.username = None | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         self.cancel_autoreconnect() | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |         if self.server and not self.server.socket.closed: | 
					
						
							|  |  |  |             await self.server.socket.close() | 
					
						
							|  |  |  |         if self.server_task: | 
					
						
							|  |  |  |             await self.server_task | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         while self.input_requests > 0: | 
					
						
							|  |  |  |             self.input_queue.put_nowait(None) | 
					
						
							|  |  |  |             self.input_requests -= 1 | 
					
						
							|  |  |  |         self.keep_alive_task.cancel() | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |         if self.ui_task: | 
					
						
							|  |  |  |             await self.ui_task | 
					
						
							|  |  |  |         if self.input_task: | 
					
						
							|  |  |  |             self.input_task.cancel() | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |     # DataPackage | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     async def prepare_data_package(self, relevant_games: typing.Set[str], | 
					
						
							|  |  |  |                                    remote_date_package_versions: typing.Dict[str, int], | 
					
						
							|  |  |  |                                    remote_data_package_checksums: typing.Dict[str, str]): | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |         """Validate that all data is present for the current multiworld.
 | 
					
						
							|  |  |  |         Download, assimilate and cache missing data from the server."""
 | 
					
						
							| 
									
										
										
										
											2022-06-20 22:30:46 +02:00
										 |  |  |         # by documentation any game can use Archipelago locations/items -> always relevant | 
					
						
							|  |  |  |         relevant_games.add("Archipelago") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |         needed_updates: typing.Set[str] = set() | 
					
						
							|  |  |  |         for game in relevant_games: | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             if game not in remote_date_package_versions and game not in remote_data_package_checksums: | 
					
						
							| 
									
										
										
										
											2022-08-29 17:16:13 -05:00
										 |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             remote_version: int = remote_date_package_versions.get(game, 0) | 
					
						
							|  |  |  |             remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if remote_version == 0 and not remote_checksum:  # custom data package and no checksum for this game | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |                 needed_updates.add(game) | 
					
						
							|  |  |  |                 continue | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |             local_version: int = network_data_package["games"].get(game, {}).get("version", 0) | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |             # no action required if local version is new enough | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ | 
					
						
							|  |  |  |                     or remote_checksum != local_checksum: | 
					
						
							|  |  |  |                 cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) | 
					
						
							|  |  |  |                 cache_version: int = cached_game.get("version", 0) | 
					
						
							|  |  |  |                 cache_checksum: typing.Optional[str] = cached_game.get("checksum") | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |                 # download remote version if cache is not new enough | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |                 if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ | 
					
						
							|  |  |  |                         or remote_checksum != cache_checksum: | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |                     needed_updates.add(game) | 
					
						
							|  |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |                     self.update_game(cached_game) | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |         if needed_updates: | 
					
						
							|  |  |  |             await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def update_game(self, game_package: dict): | 
					
						
							|  |  |  |         for item_name, item_id in game_package["item_name_to_id"].items(): | 
					
						
							|  |  |  |             self.item_names[item_id] = item_name | 
					
						
							|  |  |  |         for location_name, location_id in game_package["location_name_to_id"].items(): | 
					
						
							|  |  |  |             self.location_names[location_id] = location_name | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     def update_data_package(self, data_package: dict): | 
					
						
							|  |  |  |         for game, game_data in data_package["games"].items(): | 
					
						
							|  |  |  |             self.update_game(game_data) | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |     def consume_network_data_package(self, data_package: dict): | 
					
						
							|  |  |  |         self.update_data_package(data_package) | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |         current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {}) | 
					
						
							|  |  |  |         current_cache.update(data_package["games"]) | 
					
						
							|  |  |  |         Utils.persistent_store("datapackage", "games", current_cache) | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         for game, game_data in data_package["games"].items(): | 
					
						
							|  |  |  |             Utils.store_data_package_for_checksum(game, game_data) | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-29 15:06:58 +02:00
										 |  |  |     # data storage | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def set_notify(self, *keys: str) -> None: | 
					
						
							|  |  |  |         """Subscribe to be notified of changes to selected data storage keys.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the | 
					
						
							|  |  |  |         names of the data storage keys to the latest values received from the server. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if new_keys := (set(keys) - self.stored_data_notification_keys): | 
					
						
							|  |  |  |             self.stored_data_notification_keys.update(new_keys) | 
					
						
							|  |  |  |             async_start(self.send_msgs([{"cmd": "Get", | 
					
						
							|  |  |  |                                          "keys": list(new_keys)}, | 
					
						
							|  |  |  |                                         {"cmd": "SetNotify", | 
					
						
							|  |  |  |                                          "keys": list(new_keys)}])) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-29 21:35:06 +01:00
										 |  |  |     # DeathLink hooks | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  |         """Gets dispatched when a new DeathLink is triggered by another linked player.""" | 
					
						
							| 
									
										
										
										
											2021-11-04 13:23:13 +01:00
										 |  |  |         self.last_death_link = max(data["time"], self.last_death_link) | 
					
						
							| 
									
										
										
										
											2021-11-06 11:19:49 +01:00
										 |  |  |         text = data.get("cause", "") | 
					
						
							|  |  |  |         if text: | 
					
						
							|  |  |  |             logger.info(f"DeathLink: {text}") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             logger.info(f"DeathLink: Received from {data['source']}") | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-06 16:17:10 +01:00
										 |  |  |     async def send_death(self, death_text: str = ""): | 
					
						
							| 
									
										
										
										
											2022-02-24 06:17:39 +01:00
										 |  |  |         if self.server and self.server.socket: | 
					
						
							|  |  |  |             logger.info("DeathLink: Sending death to your friends...") | 
					
						
							|  |  |  |             self.last_death_link = time.time() | 
					
						
							|  |  |  |             await self.send_msgs([{ | 
					
						
							|  |  |  |                 "cmd": "Bounce", "tags": ["DeathLink"], | 
					
						
							|  |  |  |                 "data": { | 
					
						
							|  |  |  |                     "time": self.last_death_link, | 
					
						
							|  |  |  |                     "source": self.player_names[self.slot], | 
					
						
							|  |  |  |                     "cause": death_text | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             }]) | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-24 05:22:52 +02:00
										 |  |  |     async def update_death_link(self, death_link: bool): | 
					
						
							| 
									
										
										
										
											2021-11-24 01:38:58 -08:00
										 |  |  |         old_tags = self.tags.copy() | 
					
						
							|  |  |  |         if death_link: | 
					
						
							|  |  |  |             self.tags.add("DeathLink") | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.tags -= {"DeathLink"} | 
					
						
							|  |  |  |         if old_tags != self.tags and self.server and not self.server.socket.closed: | 
					
						
							|  |  |  |             await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: | 
					
						
							| 
									
										
										
										
											2022-06-07 00:15:08 +02:00
										 |  |  |         """Displays an error messagebox""" | 
					
						
							|  |  |  |         if not self.ui: | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |             return None | 
					
						
							| 
									
										
										
										
											2022-06-07 00:15:08 +02:00
										 |  |  |         title = title or "Error" | 
					
						
							|  |  |  |         from kvui import MessageBox | 
					
						
							|  |  |  |         if self._messagebox: | 
					
						
							|  |  |  |             self._messagebox.dismiss() | 
					
						
							|  |  |  |         # make "Multiple exceptions" look nice | 
					
						
							|  |  |  |         text = str(text).replace('[Errno', '\n[Errno').strip() | 
					
						
							|  |  |  |         # split long messages into title and text | 
					
						
							|  |  |  |         parts = title.split('. ', 1) | 
					
						
							|  |  |  |         if len(parts) == 1: | 
					
						
							|  |  |  |             parts = title.split(', ', 1) | 
					
						
							|  |  |  |         if len(parts) > 1: | 
					
						
							|  |  |  |             text = parts[1] + '\n\n' + text | 
					
						
							|  |  |  |             title = parts[0] | 
					
						
							|  |  |  |         # display error | 
					
						
							|  |  |  |         self._messagebox = MessageBox(title, text, error=True) | 
					
						
							|  |  |  |         self._messagebox.open() | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         return self._messagebox | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |     def handle_connection_loss(self, msg: str) -> None: | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         """Helper for logging and displaying a loss of connection. Must be called from an except block.""" | 
					
						
							|  |  |  |         exc_info = sys.exc_info() | 
					
						
							|  |  |  |         logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) | 
					
						
							|  |  |  |         self._messagebox_connection_loss = self.gui_error(msg, exc_info[1]) | 
					
						
							| 
									
										
										
										
											2022-06-07 00:15:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |     def run_gui(self): | 
					
						
							|  |  |  |         """Import kivy UI system and start running it as self.ui_task.""" | 
					
						
							|  |  |  |         from kvui import GameManager | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         class TextManager(GameManager): | 
					
						
							|  |  |  |             logging_pairs = [ | 
					
						
							|  |  |  |                 ("Client", "Archipelago") | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |             base_title = "Archipelago Text Client" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.ui = TextManager(self) | 
					
						
							|  |  |  |         self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def run_cli(self): | 
					
						
							|  |  |  |         if sys.stdin: | 
					
						
							|  |  |  |             # steam overlay breaks when starting console_loop | 
					
						
							|  |  |  |             if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''): | 
					
						
							|  |  |  |                 logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.") | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 self.input_task = asyncio.create_task(console_loop(self), name="Input") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-12 21:15:03 +02:00
										 |  |  | async def keep_alive(ctx: CommonContext, seconds_between_checks=100): | 
					
						
							|  |  |  |     """some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
 | 
					
						
							|  |  |  |      so we send a payload to prevent drop and if we were dropped anyway this will cause an auto-reconnect."""
 | 
					
						
							|  |  |  |     seconds_elapsed = 0 | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  |     while not ctx.exit_event.is_set(): | 
					
						
							| 
									
										
										
										
											2021-09-12 21:15:03 +02:00
										 |  |  |         await asyncio.sleep(1)  # short sleep to not block program shutdown | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  |         if ctx.server and ctx.slot: | 
					
						
							| 
									
										
										
										
											2021-09-12 21:15:03 +02:00
										 |  |  |             seconds_elapsed += 1 | 
					
						
							|  |  |  |             if seconds_elapsed > seconds_between_checks: | 
					
						
							|  |  |  |                 await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot]}]) | 
					
						
							|  |  |  |                 seconds_elapsed = 0 | 
					
						
							| 
									
										
										
										
											2021-09-11 03:59:12 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  | async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) -> None: | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     if ctx.server and ctx.server.socket: | 
					
						
							|  |  |  |         logger.error('Already connected') | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if address is None:  # set through CLI or APBP | 
					
						
							|  |  |  |         address = ctx.server_address | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Wait for the user to provide a multiworld server address | 
					
						
							|  |  |  |     if not address: | 
					
						
							|  |  |  |         logger.info('Please connect to an Archipelago server.') | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     ctx.cancel_autoreconnect() | 
					
						
							|  |  |  |     if ctx._messagebox_connection_loss: | 
					
						
							|  |  |  |         ctx._messagebox_connection_loss.dismiss() | 
					
						
							|  |  |  |         ctx._messagebox_connection_loss = None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-27 03:10:41 -07:00
										 |  |  |     address = f"ws://{address}" if "://" not in address \ | 
					
						
							|  |  |  |         else address.replace("archipelago://", "ws://") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     server_url = urllib.parse.urlparse(address) | 
					
						
							|  |  |  |     if server_url.username: | 
					
						
							|  |  |  |         ctx.username = server_url.username | 
					
						
							|  |  |  |     if server_url.password: | 
					
						
							|  |  |  |         ctx.password = server_url.password | 
					
						
							|  |  |  |     port = server_url.port or 38281 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     def reconnect_hint() -> str: | 
					
						
							|  |  |  |         return ", type /connect to reconnect" if ctx.server_address else "" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     logger.info(f'Connecting to Archipelago server at {address}') | 
					
						
							|  |  |  |     try: | 
					
						
							| 
									
										
										
										
											2023-06-22 00:01:41 +02:00
										 |  |  |         socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None, | 
					
						
							|  |  |  |                                           ssl=get_ssl_context() if address.startswith("wss://") else None) | 
					
						
							| 
									
										
										
										
											2022-08-18 01:10:33 +02:00
										 |  |  |         if ctx.ui is not None: | 
					
						
							|  |  |  |             ctx.ui.update_address_bar(server_url.netloc) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         ctx.server = Endpoint(socket) | 
					
						
							|  |  |  |         logger.info('Connected') | 
					
						
							|  |  |  |         ctx.server_address = address | 
					
						
							|  |  |  |         ctx.current_reconnect_delay = ctx.starting_reconnect_delay | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         ctx.disconnected_intentionally = False | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         async for data in ctx.server.socket: | 
					
						
							|  |  |  |             for msg in decode(data): | 
					
						
							|  |  |  |                 await process_server_cmd(ctx, msg) | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         logger.warning(f"Disconnected from multiworld server{reconnect_hint()}") | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |     except websockets.InvalidMessage: | 
					
						
							|  |  |  |         # probably encrypted | 
					
						
							|  |  |  |         if address.startswith("ws://"): | 
					
						
							| 
									
										
										
										
											2023-06-22 00:01:41 +02:00
										 |  |  |             # try wss | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |             await server_loop(ctx, "ws" + address[1:]) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage" | 
					
						
							|  |  |  |                                        f"{reconnect_hint()}") | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     except ConnectionRefusedError: | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |         ctx.handle_connection_loss("Connection refused by the server. " | 
					
						
							|  |  |  |                                    "May not be running Archipelago on that address or port.") | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     except websockets.InvalidURI: | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |         ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)") | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     except OSError: | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |         ctx.handle_connection_loss("Failed to connect to the multiworld server") | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |     except Exception: | 
					
						
							| 
									
										
										
										
											2023-01-02 20:24:54 +01:00
										 |  |  |         ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     finally: | 
					
						
							|  |  |  |         await ctx.connection_closed() | 
					
						
							| 
									
										
										
										
											2022-11-04 17:57:58 +01:00
										 |  |  |         if ctx.server_address and ctx.username and not ctx.disconnected_intentionally: | 
					
						
							|  |  |  |             logger.info(f"... automatically reconnecting in {ctx.current_reconnect_delay} seconds") | 
					
						
							|  |  |  |             assert ctx.autoreconnect_task is None | 
					
						
							|  |  |  |             ctx.autoreconnect_task = asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         ctx.current_reconnect_delay *= 2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def server_autoreconnect(ctx: CommonContext): | 
					
						
							|  |  |  |     await asyncio.sleep(ctx.current_reconnect_delay) | 
					
						
							|  |  |  |     if ctx.server_address and ctx.server_task is None: | 
					
						
							| 
									
										
										
										
											2021-11-21 02:02:40 +01:00
										 |  |  |         ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def process_server_cmd(ctx: CommonContext, args: dict): | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         cmd = args["cmd"] | 
					
						
							|  |  |  |     except: | 
					
						
							|  |  |  |         logger.exception(f"Could not get command from {args}") | 
					
						
							|  |  |  |         raise | 
					
						
							|  |  |  |     if cmd == 'RoomInfo': | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |         if ctx.seed_name and ctx.seed_name != args["seed_name"]: | 
					
						
							| 
									
										
										
										
											2022-06-07 00:15:08 +02:00
										 |  |  |             msg = "The server is running a different multiworld than your client is. (invalid seed_name)" | 
					
						
							|  |  |  |             logger.info(msg, extra={'compact_gui': True}) | 
					
						
							|  |  |  |             ctx.gui_error('Error', msg) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |             logger.info('--------------------------------') | 
					
						
							|  |  |  |             logger.info('Room Information:') | 
					
						
							|  |  |  |             logger.info('--------------------------------') | 
					
						
							|  |  |  |             version = args["version"] | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |             ctx.server_version = Version(*version) | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-25 13:26:52 +02:00
										 |  |  |             if "generator_version" in args: | 
					
						
							|  |  |  |                 ctx.generator_version = Version(*args["generator_version"]) | 
					
						
							|  |  |  |                 logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, ' | 
					
						
							|  |  |  |                             f'generator version: {ctx.generator_version.as_simple_string()}, ' | 
					
						
							|  |  |  |                             f'tags: {", ".join(args["tags"])}') | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, ' | 
					
						
							|  |  |  |                             f'tags: {", ".join(args["tags"])}') | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |             if args['password']: | 
					
						
							|  |  |  |                 logger.info('Password required') | 
					
						
							| 
									
										
										
										
											2021-10-22 05:25:09 +02:00
										 |  |  |             ctx.update_permissions(args.get("permissions", {})) | 
					
						
							| 
									
										
										
										
											2021-08-04 18:38:49 +02:00
										 |  |  |             logger.info( | 
					
						
							|  |  |  |                 f"A !hint costs {args['hint_cost']}% of your total location count as points" | 
					
						
							|  |  |  |                 f" and you get {args['location_check_points']}" | 
					
						
							|  |  |  |                 f" for each location checked. Use !hint for more information.") | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |             ctx.hint_cost = int(args['hint_cost']) | 
					
						
							|  |  |  |             ctx.check_points = int(args['location_check_points']) | 
					
						
							| 
									
										
										
										
											2022-08-10 22:20:14 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             if "players" in args:  # TODO remove when servers sending this are outdated | 
					
						
							|  |  |  |                 players = args.get("players", []) | 
					
						
							|  |  |  |                 if len(players) < 1: | 
					
						
							|  |  |  |                     logger.info('No player connected') | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     players.sort() | 
					
						
							|  |  |  |                     current_team = -1 | 
					
						
							|  |  |  |                     logger.info('Connected Players:') | 
					
						
							|  |  |  |                     for network_player in players: | 
					
						
							|  |  |  |                         if network_player.team != current_team: | 
					
						
							|  |  |  |                             logger.info(f'  Team #{network_player.team + 1}') | 
					
						
							|  |  |  |                             current_team = network_player.team | 
					
						
							|  |  |  |                         logger.info('    %s (Player %d)' % (network_player.alias, network_player.slot)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |             # update data package | 
					
						
							|  |  |  |             data_package_versions = args.get("datapackage_versions", {}) | 
					
						
							|  |  |  |             data_package_checksums = args.get("datapackage_checksums", {}) | 
					
						
							|  |  |  |             await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums) | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-16 00:21:00 +02:00
										 |  |  |             await ctx.server_auth(args['password']) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == 'DataPackage': | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |         logger.info("Got new ID/Name DataPackage") | 
					
						
							| 
									
										
										
										
											2023-03-20 11:01:08 -05:00
										 |  |  |         ctx.consume_network_data_package(args['data']) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == 'ConnectionRefused': | 
					
						
							|  |  |  |         errors = args["errors"] | 
					
						
							|  |  |  |         if 'InvalidSlot' in errors: | 
					
						
							|  |  |  |             ctx.event_invalid_slot() | 
					
						
							| 
									
										
										
										
											2021-09-17 04:32:09 +02:00
										 |  |  |         elif 'InvalidGame' in errors: | 
					
						
							|  |  |  |             ctx.event_invalid_game() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         elif 'IncompatibleVersion' in errors: | 
					
						
							|  |  |  |             raise Exception('Server reported your client version as incompatible') | 
					
						
							| 
									
										
										
										
											2022-01-29 15:39:14 +01:00
										 |  |  |         elif 'InvalidItemsHandling' in errors: | 
					
						
							|  |  |  |             raise Exception('The item handling flags requested by the client are not supported') | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         # last to check, recoverable problem | 
					
						
							|  |  |  |         elif 'InvalidPassword' in errors: | 
					
						
							|  |  |  |             logger.error('Invalid password') | 
					
						
							|  |  |  |             ctx.password = None | 
					
						
							|  |  |  |             await ctx.server_auth(True) | 
					
						
							| 
									
										
										
										
											2021-06-25 07:25:03 +02:00
										 |  |  |         elif errors: | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |             raise Exception("Unknown connection errors: " + str(errors)) | 
					
						
							| 
									
										
										
										
											2021-06-25 07:25:03 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             raise Exception('Connection refused by the multiworld host, no reason provided') | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == 'Connected': | 
					
						
							| 
									
										
										
										
											2022-06-27 03:10:41 -07:00
										 |  |  |         ctx.username = ctx.auth | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         ctx.team = args["team"] | 
					
						
							|  |  |  |         ctx.slot = args["slot"] | 
					
						
							| 
									
										
										
										
											2022-05-24 00:20:02 +02:00
										 |  |  |         # int keys get lost in JSON transfer | 
					
						
							|  |  |  |         ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()} | 
					
						
							| 
									
										
										
										
											2023-04-10 14:44:20 -05:00
										 |  |  |         ctx.hint_points = args.get("hint_points", 0) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         ctx.consume_players_package(args["players"]) | 
					
						
							|  |  |  |         msgs = [] | 
					
						
							|  |  |  |         if ctx.locations_checked: | 
					
						
							|  |  |  |             msgs.append({"cmd": "LocationChecks", | 
					
						
							|  |  |  |                          "locations": list(ctx.locations_checked)}) | 
					
						
							|  |  |  |         if ctx.locations_scouted: | 
					
						
							|  |  |  |             msgs.append({"cmd": "LocationScouts", | 
					
						
							|  |  |  |                          "locations": list(ctx.locations_scouted)}) | 
					
						
							| 
									
										
										
										
											2023-06-29 15:06:58 +02:00
										 |  |  |         if ctx.stored_data_notification_keys: | 
					
						
							|  |  |  |             msgs.append({"cmd": "Get", | 
					
						
							|  |  |  |                          "keys": list(ctx.stored_data_notification_keys)}) | 
					
						
							|  |  |  |             msgs.append({"cmd": "SetNotify", | 
					
						
							|  |  |  |                          "keys": list(ctx.stored_data_notification_keys)}) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         if msgs: | 
					
						
							|  |  |  |             await ctx.send_msgs(msgs) | 
					
						
							|  |  |  |         if ctx.finished_game: | 
					
						
							| 
									
										
										
										
											2021-04-03 20:02:15 +02:00
										 |  |  |             await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         # Get the server side view of missing as of time of connecting. | 
					
						
							|  |  |  |         # This list is used to only send to the server what is reported as ACTUALLY Missing. | 
					
						
							|  |  |  |         # This also serves to allow an easy visual of what locations were already checked previously | 
					
						
							|  |  |  |         # when /missing is used for the client side view of what is missing. | 
					
						
							| 
									
										
										
										
											2021-10-18 22:58:29 +02:00
										 |  |  |         ctx.missing_locations = set(args["missing_locations"]) | 
					
						
							|  |  |  |         ctx.checked_locations = set(args["checked_locations"]) | 
					
						
							| 
									
										
										
										
											2022-08-31 20:55:15 +02:00
										 |  |  |         ctx.server_locations = ctx.missing_locations | ctx. checked_locations | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-11-01 06:54:40 +01:00
										 |  |  |         server_url = urllib.parse.urlparse(ctx.server_address) | 
					
						
							|  |  |  |         Utils.persistent_store("client", "last_server_address", server_url.netloc) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     elif cmd == 'ReceivedItems': | 
					
						
							|  |  |  |         start_index = args["index"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if start_index == 0: | 
					
						
							|  |  |  |             ctx.items_received = [] | 
					
						
							|  |  |  |         elif start_index != len(ctx.items_received): | 
					
						
							|  |  |  |             sync_msg = [{'cmd': 'Sync'}] | 
					
						
							|  |  |  |             if ctx.locations_checked: | 
					
						
							|  |  |  |                 sync_msg.append({"cmd": "LocationChecks", | 
					
						
							|  |  |  |                                  "locations": list(ctx.locations_checked)}) | 
					
						
							|  |  |  |             await ctx.send_msgs(sync_msg) | 
					
						
							|  |  |  |         if start_index == len(ctx.items_received): | 
					
						
							|  |  |  |             for item in args['items']: | 
					
						
							|  |  |  |                 ctx.items_received.append(NetworkItem(*item)) | 
					
						
							|  |  |  |         ctx.watcher_event.set() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == 'LocationInfo': | 
					
						
							| 
									
										
										
										
											2022-03-07 11:21:29 -08:00
										 |  |  |         for item in [NetworkItem(*item) for item in args['locations']]: | 
					
						
							| 
									
										
										
										
											2022-03-21 15:26:38 +01:00
										 |  |  |             ctx.locations_info[item.location] = item | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         ctx.watcher_event.set() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == "RoomUpdate": | 
					
						
							|  |  |  |         if "players" in args: | 
					
						
							|  |  |  |             ctx.consume_players_package(args["players"]) | 
					
						
							|  |  |  |         if "hint_points" in args: | 
					
						
							|  |  |  |             ctx.hint_points = args['hint_points'] | 
					
						
							| 
									
										
										
										
											2021-10-22 05:25:09 +02:00
										 |  |  |         if "checked_locations" in args: | 
					
						
							|  |  |  |             checked = set(args["checked_locations"]) | 
					
						
							|  |  |  |             ctx.checked_locations |= checked | 
					
						
							|  |  |  |             ctx.missing_locations -= checked | 
					
						
							|  |  |  |         if "permissions" in args: | 
					
						
							|  |  |  |             ctx.update_permissions(args["permissions"]) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == 'Print': | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |         ctx.on_print(args) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == 'PrintJSON': | 
					
						
							| 
									
										
										
										
											2021-04-13 14:49:32 +02:00
										 |  |  |         ctx.on_print_json(args) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-14 10:02:39 +02:00
										 |  |  |     elif cmd == 'InvalidPacket': | 
					
						
							|  |  |  |         logger.warning(f"Invalid Packet of {args['type']}: {args['text']}") | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-02 01:35:24 +02:00
										 |  |  |     elif cmd == "Bounced": | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  |         tags = args.get("tags", []) | 
					
						
							| 
									
										
										
										
											2021-11-02 11:11:57 +01:00
										 |  |  |         # we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  |         if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]: | 
					
						
							|  |  |  |             ctx.on_deathlink(args["data"]) | 
					
						
							| 
									
										
										
										
											2023-06-29 15:06:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     elif cmd == "Retrieved": | 
					
						
							|  |  |  |         ctx.stored_data.update(args["keys"]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |     elif cmd == "SetReply": | 
					
						
							| 
									
										
										
										
											2023-06-29 15:06:58 +02:00
										 |  |  |         ctx.stored_data[args["key"]] = args["value"] | 
					
						
							| 
									
										
										
										
											2022-02-24 04:47:01 +01:00
										 |  |  |         if args["key"] == "EnergyLink": | 
					
						
							|  |  |  |             ctx.current_energy_link_value = args["value"] | 
					
						
							|  |  |  |             if ctx.ui: | 
					
						
							|  |  |  |                 ctx.ui.set_new_energy_link_value() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     else: | 
					
						
							|  |  |  |         logger.debug(f"unknown command {cmd}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-31 19:45:17 +02:00
										 |  |  |     ctx.on_package(cmd, args) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | async def console_loop(ctx: CommonContext): | 
					
						
							|  |  |  |     commandprocessor = ctx.command_processor(ctx) | 
					
						
							| 
									
										
										
										
											2021-11-28 03:27:18 +01:00
										 |  |  |     queue = asyncio.Queue() | 
					
						
							|  |  |  |     stream_input(sys.stdin, queue) | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |     while not ctx.exit_event.is_set(): | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2021-11-28 03:27:18 +01:00
										 |  |  |             input_text = await queue.get() | 
					
						
							| 
									
										
										
										
											2021-11-28 04:06:30 +01:00
										 |  |  |             queue.task_done() | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |             if ctx.input_requests > 0: | 
					
						
							|  |  |  |                 ctx.input_requests -= 1 | 
					
						
							|  |  |  |                 ctx.input_queue.put_nowait(input_text) | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if input_text: | 
					
						
							|  |  |  |                 commandprocessor(input_text) | 
					
						
							|  |  |  |         except Exception as e: | 
					
						
							| 
									
										
										
										
											2021-07-31 19:45:17 +02:00
										 |  |  |             logger.exception(e) | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  | def get_base_parser(description: typing.Optional[str] = None): | 
					
						
							| 
									
										
										
										
											2021-11-09 12:53:05 +01:00
										 |  |  |     import argparse | 
					
						
							|  |  |  |     parser = argparse.ArgumentParser(description=description) | 
					
						
							|  |  |  |     parser.add_argument('--connect', default=None, help='Address of the multiworld host.') | 
					
						
							|  |  |  |     parser.add_argument('--password', default=None, help='Password of the multiworld host.') | 
					
						
							|  |  |  |     if sys.stdout:  # If terminal output exists, offer gui-less mode | 
					
						
							|  |  |  |         parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.") | 
					
						
							|  |  |  |     return parser | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-25 02:31:25 +02:00
										 |  |  | def run_as_textclient(): | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |     class TextContext(CommonContext): | 
					
						
							| 
									
										
										
										
											2023-06-25 02:31:25 +02:00
										 |  |  |         # Text Mode to use !hint and such with games that have no text entry | 
					
						
							| 
									
										
										
										
											2022-12-03 04:22:58 +01:00
										 |  |  |         tags = {"AP", "TextOnly"} | 
					
						
							| 
									
										
										
										
											2022-04-08 11:16:36 +02:00
										 |  |  |         game = ""  # empty matches any game since 0.3.2 | 
					
						
							| 
									
										
										
										
											2022-08-10 23:05:36 +02:00
										 |  |  |         items_handling = 0b111  # receive all items for /received | 
					
						
							| 
									
										
										
										
											2022-12-03 23:29:33 +01:00
										 |  |  |         want_slot_data = False  # Can't use game specific slot_data | 
					
						
							| 
									
										
										
										
											2021-10-25 09:58:08 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |         async def server_auth(self, password_requested: bool = False): | 
					
						
							|  |  |  |             if password_requested and not self.password: | 
					
						
							|  |  |  |                 await super(TextContext, self).server_auth(password_requested) | 
					
						
							| 
									
										
										
										
											2022-06-27 03:10:41 -07:00
										 |  |  |             await self.get_username() | 
					
						
							| 
									
										
										
										
											2021-11-21 02:50:24 +01:00
										 |  |  |             await self.send_connect() | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-30 07:33:05 +02:00
										 |  |  |         def on_package(self, cmd: str, args: dict): | 
					
						
							|  |  |  |             if cmd == "Connected": | 
					
						
							| 
									
										
										
										
											2022-06-09 12:54:03 +02:00
										 |  |  |                 self.game = self.slot_info[self.slot].game | 
					
						
							| 
									
										
										
										
											2023-06-25 02:31:25 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-28 00:32:48 +01:00
										 |  |  |         async def disconnect(self, allow_autoreconnect: bool = False): | 
					
						
							|  |  |  |             self.game = "" | 
					
						
							|  |  |  |             await super().disconnect(allow_autoreconnect) | 
					
						
							| 
									
										
										
										
											2021-10-30 07:33:05 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |     async def main(args): | 
					
						
							|  |  |  |         ctx = TextContext(args.connect, args.password) | 
					
						
							| 
									
										
										
										
											2022-05-24 00:20:02 +02:00
										 |  |  |         ctx.auth = args.name | 
					
						
							| 
									
										
										
										
											2021-11-21 02:02:40 +01:00
										 |  |  |         ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop") | 
					
						
							| 
									
										
										
										
											2022-04-06 00:06:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |         if gui_enabled: | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |             ctx.run_gui() | 
					
						
							|  |  |  |         ctx.run_cli() | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |         await ctx.exit_event.wait() | 
					
						
							| 
									
										
										
										
											2021-11-21 02:02:40 +01:00
										 |  |  |         await ctx.shutdown() | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     import colorama | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-26 06:02:03 +01:00
										 |  |  |     parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.") | 
					
						
							| 
									
										
										
										
											2022-05-24 00:20:02 +02:00
										 |  |  |     parser.add_argument('--name', default=None, help="Slot Name to connect as.") | 
					
						
							|  |  |  |     parser.add_argument("url", nargs="?", help="Archipelago connection url") | 
					
						
							|  |  |  |     args = parser.parse_args() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if args.url: | 
					
						
							|  |  |  |         url = urllib.parse.urlparse(args.url) | 
					
						
							|  |  |  |         args.connect = url.netloc | 
					
						
							| 
									
										
										
										
											2022-05-24 05:22:52 +02:00
										 |  |  |         if url.username: | 
					
						
							|  |  |  |             args.name = urllib.parse.unquote(url.username) | 
					
						
							|  |  |  |         if url.password: | 
					
						
							|  |  |  |             args.password = urllib.parse.unquote(url.password) | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     colorama.init() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-27 22:11:11 +02:00
										 |  |  |     asyncio.run(main(args)) | 
					
						
							| 
									
										
										
										
											2021-09-30 09:09:21 +02:00
										 |  |  |     colorama.deinit() | 
					
						
							| 
									
										
										
										
											2023-06-25 02:31:25 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  |     run_as_textclient() |