| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | import asyncio | 
					
						
							|  |  |  | import hashlib | 
					
						
							|  |  |  | import json | 
					
						
							|  |  |  | import time | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import bsdiff4 | 
					
						
							|  |  |  | import subprocess | 
					
						
							|  |  |  | import zipfile | 
					
						
							|  |  |  | from asyncio import StreamReader, StreamWriter, CancelledError | 
					
						
							|  |  |  | from typing import List | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import Utils | 
					
						
							|  |  |  | from NetUtils import ClientStatus | 
					
						
							|  |  |  | from Utils import async_start | 
					
						
							|  |  |  | from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ | 
					
						
							|  |  |  |     get_base_parser | 
					
						
							|  |  |  | from worlds.adventure import AdventureDeltaPatch | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from worlds.adventure.Locations import base_location_id | 
					
						
							|  |  |  | from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation | 
					
						
							|  |  |  | from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table | 
					
						
							|  |  |  | from worlds.adventure.Offsets import static_item_element_size, connector_port_offset | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | SYSTEM_MESSAGE_ID = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | CONNECTION_TIMING_OUT_STATUS = \ | 
					
						
							| 
									
										
										
										
											2023-04-15 00:17:33 -07:00
										 |  |  |     "Connection timing out. Please restart your emulator, then restart connector_adventure.lua" | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | CONNECTION_REFUSED_STATUS = \ | 
					
						
							| 
									
										
										
										
											2023-04-15 00:17:33 -07:00
										 |  |  |     "Connection Refused. Please start your emulator and make sure connector_adventure.lua is running" | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | CONNECTION_RESET_STATUS = \ | 
					
						
							| 
									
										
										
										
											2023-04-15 00:17:33 -07:00
										 |  |  |     "Connection was reset. Please restart your emulator, then restart connector_adventure.lua" | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  | CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" | 
					
						
							|  |  |  | CONNECTION_CONNECTED_STATUS = "Connected" | 
					
						
							|  |  |  | CONNECTION_INITIAL_STATUS = "Connection has not been initiated" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | SCRIPT_VERSION = 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class AdventureCommandProcessor(ClientCommandProcessor): | 
					
						
							|  |  |  |     def __init__(self, ctx: CommonContext): | 
					
						
							|  |  |  |         super().__init__(ctx) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_2600(self): | 
					
						
							|  |  |  |         """Check 2600 Connection State""" | 
					
						
							|  |  |  |         if isinstance(self.ctx, AdventureContext): | 
					
						
							|  |  |  |             logger.info(f"2600 Status: {self.ctx.atari_status}") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _cmd_aconnect(self): | 
					
						
							|  |  |  |         """Discard current atari 2600 connection state""" | 
					
						
							|  |  |  |         if isinstance(self.ctx, AdventureContext): | 
					
						
							|  |  |  |             self.ctx.atari_sync_task.cancel() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class AdventureContext(CommonContext): | 
					
						
							|  |  |  |     command_processor = AdventureCommandProcessor | 
					
						
							|  |  |  |     game = 'Adventure' | 
					
						
							|  |  |  |     lua_connector_port: int = 17242 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, server_address, password): | 
					
						
							|  |  |  |         super().__init__(server_address, password) | 
					
						
							|  |  |  |         self.freeincarnates_used: int = -1 | 
					
						
							|  |  |  |         self.freeincarnate_pending: int = 0 | 
					
						
							|  |  |  |         self.foreign_items: [AdventureForeignItemInfo] = [] | 
					
						
							|  |  |  |         self.autocollect_items: [AdventureAutoCollectLocation] = [] | 
					
						
							|  |  |  |         self.atari_streams: (StreamReader, StreamWriter) = None | 
					
						
							|  |  |  |         self.atari_sync_task = None | 
					
						
							|  |  |  |         self.messages = {} | 
					
						
							|  |  |  |         self.locations_array = None | 
					
						
							|  |  |  |         self.atari_status = CONNECTION_INITIAL_STATUS | 
					
						
							|  |  |  |         self.awaiting_rom = False | 
					
						
							|  |  |  |         self.display_msgs = True | 
					
						
							|  |  |  |         self.deathlink_pending = False | 
					
						
							|  |  |  |         self.set_deathlink = False | 
					
						
							|  |  |  |         self.client_compatibility_mode = 0 | 
					
						
							|  |  |  |         self.items_handling = 0b111 | 
					
						
							|  |  |  |         self.checked_locations_sent: bool = False | 
					
						
							|  |  |  |         self.port_offset = 0 | 
					
						
							|  |  |  |         self.bat_no_touch_locations: [BatNoTouchLocation] = [] | 
					
						
							|  |  |  |         self.local_item_locations = {} | 
					
						
							|  |  |  |         self.dragon_speed_info = {} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-10 15:42:01 -07:00
										 |  |  |         options = Utils.get_settings() | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |         self.display_msgs = options["adventure_options"]["display_msgs"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def server_auth(self, password_requested: bool = False): | 
					
						
							|  |  |  |         if password_requested and not self.password: | 
					
						
							|  |  |  |             await super(AdventureContext, self).server_auth(password_requested) | 
					
						
							|  |  |  |         if not self.auth: | 
					
						
							|  |  |  |             self.auth = self.player_name | 
					
						
							|  |  |  |         if not self.auth: | 
					
						
							|  |  |  |             self.awaiting_rom = True | 
					
						
							|  |  |  |             logger.info('Awaiting connection to adventure_connector to get Player information') | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await self.send_connect() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _set_message(self, msg: str, msg_id: int): | 
					
						
							|  |  |  |         if self.display_msgs: | 
					
						
							|  |  |  |             self.messages[(time.time(), msg_id)] = msg | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def on_package(self, cmd: str, args: dict): | 
					
						
							|  |  |  |         if cmd == 'Connected': | 
					
						
							|  |  |  |             self.locations_array = None | 
					
						
							| 
									
										
										
										
											2024-06-10 15:42:01 -07:00
										 |  |  |             if Utils.get_settings()["adventure_options"].get("death_link", False): | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |                 self.set_deathlink = True | 
					
						
							|  |  |  |             async_start(self.get_freeincarnates_used()) | 
					
						
							|  |  |  |         elif cmd == "RoomInfo": | 
					
						
							|  |  |  |             self.seed_name = args['seed_name'] | 
					
						
							|  |  |  |         elif cmd == 'Print': | 
					
						
							|  |  |  |             msg = args['text'] | 
					
						
							|  |  |  |             if ': !' not in msg: | 
					
						
							|  |  |  |                 self._set_message(msg, SYSTEM_MESSAGE_ID) | 
					
						
							|  |  |  |         elif cmd == "ReceivedItems": | 
					
						
							| 
									
										
										
										
											2024-06-16 05:37:05 -05:00
										 |  |  |             msg = f"Received {', '.join([self.item_names.lookup_in_game(item.item) for item in args['items']])}" | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |             self._set_message(msg, SYSTEM_MESSAGE_ID) | 
					
						
							|  |  |  |         elif cmd == "Retrieved": | 
					
						
							| 
									
										
										
										
											2023-12-16 15:22:51 -06:00
										 |  |  |             if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: | 
					
						
							|  |  |  |                 self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] | 
					
						
							|  |  |  |                 if self.freeincarnates_used is None: | 
					
						
							|  |  |  |                     self.freeincarnates_used = 0 | 
					
						
							|  |  |  |                 self.freeincarnates_used += self.freeincarnate_pending | 
					
						
							|  |  |  |                 self.send_pending_freeincarnates() | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |         elif cmd == "SetReply": | 
					
						
							|  |  |  |             if args["key"] == f"adventure_{self.auth}_freeincarnates_used": | 
					
						
							|  |  |  |                 self.freeincarnates_used = args["value"] | 
					
						
							|  |  |  |                 if self.freeincarnates_used is None: | 
					
						
							|  |  |  |                     self.freeincarnates_used = 0 | 
					
						
							|  |  |  |                 self.freeincarnates_used += self.freeincarnate_pending | 
					
						
							|  |  |  |                 self.send_pending_freeincarnates() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def on_deathlink(self, data: dict): | 
					
						
							|  |  |  |         self.deathlink_pending = True | 
					
						
							|  |  |  |         super().on_deathlink(data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def run_gui(self): | 
					
						
							|  |  |  |         from kvui import GameManager | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         class AdventureManager(GameManager): | 
					
						
							|  |  |  |             logging_pairs = [ | 
					
						
							|  |  |  |                 ("Client", "Archipelago") | 
					
						
							|  |  |  |             ] | 
					
						
							|  |  |  |             base_title = "Archipelago Adventure Client" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.ui = AdventureManager(self) | 
					
						
							|  |  |  |         self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def get_freeincarnates_used(self): | 
					
						
							|  |  |  |         if self.server and not self.server.socket.closed: | 
					
						
							|  |  |  |             await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}]) | 
					
						
							|  |  |  |             await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def send_pending_freeincarnates(self): | 
					
						
							|  |  |  |         if self.freeincarnate_pending > 0: | 
					
						
							|  |  |  |             async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending)) | 
					
						
							|  |  |  |             self.freeincarnate_pending = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def send_pending_freeincarnates_impl(self, send_val: int) -> None: | 
					
						
							|  |  |  |         await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used", | 
					
						
							|  |  |  |                                "default": 0, "want_reply": False, | 
					
						
							|  |  |  |                                "operations": [{"operation": "add", "value": send_val}]}]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def used_freeincarnate(self) -> None: | 
					
						
							|  |  |  |         if self.server and not self.server.socket.closed: | 
					
						
							|  |  |  |             await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used", | 
					
						
							|  |  |  |                                    "default": 0, "want_reply": True, | 
					
						
							|  |  |  |                                    "operations": [{"operation": "add", "value": 1}]}]) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             self.freeincarnate_pending = self.freeincarnate_pending + 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def convert_item_id(ap_item_id: int): | 
					
						
							|  |  |  |     static_item_index = ap_item_id - base_adventure_item_id | 
					
						
							|  |  |  |     return static_item_index * static_item_element_size | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_payload(ctx: AdventureContext): | 
					
						
							|  |  |  |     current_time = time.time() | 
					
						
							|  |  |  |     items = [] | 
					
						
							|  |  |  |     dragon_speed_update = {} | 
					
						
							|  |  |  |     diff_a_locked = ctx.diff_a_mode > 0 | 
					
						
							|  |  |  |     diff_b_locked = ctx.diff_b_mode > 0 | 
					
						
							|  |  |  |     freeincarnate_count = 0 | 
					
						
							|  |  |  |     for item in ctx.items_received: | 
					
						
							|  |  |  |         item_id_str = str(item.item) | 
					
						
							|  |  |  |         if base_adventure_item_id < item.item <= standard_item_max: | 
					
						
							|  |  |  |             items.append(convert_item_id(item.item)) | 
					
						
							|  |  |  |         elif item_id_str in ctx.dragon_speed_info: | 
					
						
							|  |  |  |             if item.item in dragon_speed_update: | 
					
						
							|  |  |  |                 last_index = len(ctx.dragon_speed_info[item_id_str]) - 1 | 
					
						
							|  |  |  |                 dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index] | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0] | 
					
						
							|  |  |  |         elif item.item == item_table["Left Difficulty Switch"].id: | 
					
						
							|  |  |  |             diff_a_locked = False | 
					
						
							|  |  |  |         elif item.item == item_table["Right Difficulty Switch"].id: | 
					
						
							|  |  |  |             diff_b_locked = False | 
					
						
							|  |  |  |         elif item.item == item_table["Freeincarnate"].id: | 
					
						
							|  |  |  |             freeincarnate_count = freeincarnate_count + 1 | 
					
						
							|  |  |  |     freeincarnates_available = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ctx.freeincarnates_used >= 0: | 
					
						
							|  |  |  |         freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending) | 
					
						
							|  |  |  |     ret = json.dumps( | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             "items": items, | 
					
						
							|  |  |  |             "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() | 
					
						
							|  |  |  |                          if key[0] > current_time - 10}, | 
					
						
							|  |  |  |             "deathlink": ctx.deathlink_pending, | 
					
						
							|  |  |  |             "dragon_speeds": dragon_speed_update, | 
					
						
							|  |  |  |             "difficulty_a_locked": diff_a_locked, | 
					
						
							|  |  |  |             "difficulty_b_locked": diff_b_locked, | 
					
						
							|  |  |  |             "freeincarnates_available": freeincarnates_available, | 
					
						
							|  |  |  |             "bat_logic": ctx.bat_logic | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     ctx.deathlink_pending = False | 
					
						
							|  |  |  |     return ret | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def parse_locations(data: List, ctx: AdventureContext): | 
					
						
							|  |  |  |     locations = data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # for loc_name, loc_data in location_table.items(): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # if flags["EventFlag"][280] & 1 and not ctx.finished_game: | 
					
						
							|  |  |  |     #    await ctx.send_msgs([ | 
					
						
							|  |  |  |     #                {"cmd": "StatusUpdate", | 
					
						
							|  |  |  |     #                 "status": 30} | 
					
						
							|  |  |  |     #            ]) | 
					
						
							|  |  |  |     #    ctx.finished_game = True | 
					
						
							|  |  |  |     if locations == ctx.locations_array: | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  |     ctx.locations_array = locations | 
					
						
							|  |  |  |     if locations is not None: | 
					
						
							|  |  |  |         await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def send_ap_foreign_items(adventure_context): | 
					
						
							|  |  |  |     foreign_item_json_list = [] | 
					
						
							|  |  |  |     autocollect_item_json_list = [] | 
					
						
							|  |  |  |     bat_no_touch_locations_json_list = [] | 
					
						
							|  |  |  |     for fi in adventure_context.foreign_items: | 
					
						
							|  |  |  |         foreign_item_json_list.append(fi.get_dict()) | 
					
						
							|  |  |  |     for fi in adventure_context.autocollect_items: | 
					
						
							|  |  |  |         autocollect_item_json_list.append(fi.get_dict()) | 
					
						
							|  |  |  |     for ntl in adventure_context.bat_no_touch_locations: | 
					
						
							|  |  |  |         bat_no_touch_locations_json_list.append(ntl.get_dict()) | 
					
						
							|  |  |  |     payload = json.dumps( | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             "foreign_items": foreign_item_json_list, | 
					
						
							|  |  |  |             "autocollect_items": autocollect_item_json_list, | 
					
						
							|  |  |  |             "local_item_locations": adventure_context.local_item_locations, | 
					
						
							|  |  |  |             "bat_no_touch_locations": bat_no_touch_locations_json_list | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     print("sending foreign items") | 
					
						
							|  |  |  |     msg = payload.encode() | 
					
						
							|  |  |  |     (reader, writer) = adventure_context.atari_streams | 
					
						
							|  |  |  |     writer.write(msg) | 
					
						
							|  |  |  |     writer.write(b'\n') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def send_checked_locations_if_needed(adventure_context): | 
					
						
							|  |  |  |     if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None: | 
					
						
							|  |  |  |         if len(adventure_context.checked_locations) == 0: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         checked_short_ids = [] | 
					
						
							|  |  |  |         for location in adventure_context.checked_locations: | 
					
						
							|  |  |  |             checked_short_ids.append(location - base_location_id) | 
					
						
							|  |  |  |         print("Sending checked locations") | 
					
						
							|  |  |  |         payload = json.dumps( | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 "checked_locations": checked_short_ids, | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         msg = payload.encode() | 
					
						
							|  |  |  |         (reader, writer) = adventure_context.atari_streams | 
					
						
							|  |  |  |         writer.write(msg) | 
					
						
							|  |  |  |         writer.write(b'\n') | 
					
						
							|  |  |  |         adventure_context.checked_locations_sent = True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def atari_sync_task(ctx: AdventureContext): | 
					
						
							|  |  |  |     logger.info("Starting Atari 2600 connector. Use /2600 for status information") | 
					
						
							|  |  |  |     while not ctx.exit_event.is_set(): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             error_status = None | 
					
						
							|  |  |  |             if ctx.atari_streams: | 
					
						
							|  |  |  |                 (reader, writer) = ctx.atari_streams | 
					
						
							|  |  |  |                 msg = get_payload(ctx).encode() | 
					
						
							|  |  |  |                 writer.write(msg) | 
					
						
							|  |  |  |                 writer.write(b'\n') | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     await asyncio.wait_for(writer.drain(), timeout=1.5) | 
					
						
							|  |  |  |                     try: | 
					
						
							|  |  |  |                         # Data will return a dict with 1+ fields | 
					
						
							|  |  |  |                         # 1. A keepalive response of the Players Name (always) | 
					
						
							|  |  |  |                         # 2. romhash field with sha256 hash of the ROM memory region | 
					
						
							|  |  |  |                         # 3. locations, messages, and deathLink | 
					
						
							|  |  |  |                         # 4. freeincarnate, to indicate a freeincarnate was used | 
					
						
							|  |  |  |                         data = await asyncio.wait_for(reader.readline(), timeout=5) | 
					
						
							|  |  |  |                         data_decoded = json.loads(data.decode()) | 
					
						
							|  |  |  |                         if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION: | 
					
						
							|  |  |  |                             msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \ | 
					
						
							|  |  |  |                                   "Lua and AdventureClient are from the same Archipelago installation." | 
					
						
							|  |  |  |                             logger.info(msg, extra={'compact_gui': True}) | 
					
						
							|  |  |  |                             ctx.gui_error('Error', msg) | 
					
						
							|  |  |  |                             error_status = CONNECTION_RESET_STATUS | 
					
						
							|  |  |  |                         if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data: | 
					
						
							|  |  |  |                             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) | 
					
						
							|  |  |  |                             error_status = CONNECTION_RESET_STATUS | 
					
						
							|  |  |  |                         if 'romhash' in data_decoded: | 
					
						
							|  |  |  |                             if ctx.rom_hash.upper() != data_decoded['romhash'].upper(): | 
					
						
							|  |  |  |                                 msg = "The rom hash does not match the client rom hash data" | 
					
						
							|  |  |  |                                 print("got " + data_decoded['romhash']) | 
					
						
							|  |  |  |                                 print("expected " + str(ctx.rom_hash)) | 
					
						
							|  |  |  |                                 logger.info(msg, extra={'compact_gui': True}) | 
					
						
							|  |  |  |                                 ctx.gui_error('Error', msg) | 
					
						
							|  |  |  |                                 error_status = CONNECTION_RESET_STATUS | 
					
						
							|  |  |  |                                 if ctx.auth is None: | 
					
						
							|  |  |  |                                     ctx.auth = ctx.player_name | 
					
						
							|  |  |  |                             if ctx.awaiting_rom: | 
					
						
							|  |  |  |                                 await ctx.server_auth(False) | 
					
						
							|  |  |  |                         if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \ | 
					
						
							|  |  |  |                                 and not error_status and ctx.auth: | 
					
						
							|  |  |  |                             # Not just a keep alive ping, parse | 
					
						
							|  |  |  |                             async_start(parse_locations(data_decoded['locations'], ctx)) | 
					
						
							|  |  |  |                         if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags: | 
					
						
							|  |  |  |                             dragon_name = "a dragon" | 
					
						
							|  |  |  |                             if data_decoded['deathLink'] == 1: | 
					
						
							|  |  |  |                                 dragon_name = "Rhindle" | 
					
						
							|  |  |  |                             elif data_decoded['deathLink'] == 2: | 
					
						
							|  |  |  |                                 dragon_name = "Yorgle" | 
					
						
							|  |  |  |                             elif data_decoded['deathLink'] == 3: | 
					
						
							|  |  |  |                                 dragon_name = "Grundle" | 
					
						
							|  |  |  |                             print (ctx.auth + " has been eaten by " + dragon_name ) | 
					
						
							|  |  |  |                             await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name) | 
					
						
							|  |  |  |                             # TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by ' | 
					
						
							|  |  |  |                         if 'victory' in data_decoded and not ctx.finished_game: | 
					
						
							|  |  |  |                             await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) | 
					
						
							|  |  |  |                             ctx.finished_game = True | 
					
						
							|  |  |  |                         if 'freeincarnate' in data_decoded: | 
					
						
							|  |  |  |                             await ctx.used_freeincarnate() | 
					
						
							|  |  |  |                         if ctx.set_deathlink: | 
					
						
							|  |  |  |                             await ctx.update_death_link(True) | 
					
						
							|  |  |  |                         send_checked_locations_if_needed(ctx) | 
					
						
							|  |  |  |                     except asyncio.TimeoutError: | 
					
						
							|  |  |  |                         logger.debug("Read Timed Out, Reconnecting") | 
					
						
							|  |  |  |                         error_status = CONNECTION_TIMING_OUT_STATUS | 
					
						
							|  |  |  |                         writer.close() | 
					
						
							|  |  |  |                         ctx.atari_streams = None | 
					
						
							|  |  |  |                     except ConnectionResetError as e: | 
					
						
							|  |  |  |                         logger.debug("Read failed due to Connection Lost, Reconnecting") | 
					
						
							|  |  |  |                         error_status = CONNECTION_RESET_STATUS | 
					
						
							|  |  |  |                         writer.close() | 
					
						
							|  |  |  |                         ctx.atari_streams = None | 
					
						
							|  |  |  |                 except TimeoutError: | 
					
						
							|  |  |  |                     logger.debug("Connection Timed Out, Reconnecting") | 
					
						
							|  |  |  |                     error_status = CONNECTION_TIMING_OUT_STATUS | 
					
						
							|  |  |  |                     writer.close() | 
					
						
							|  |  |  |                     ctx.atari_streams = None | 
					
						
							|  |  |  |                 except ConnectionResetError: | 
					
						
							|  |  |  |                     logger.debug("Connection Lost, Reconnecting") | 
					
						
							|  |  |  |                     error_status = CONNECTION_RESET_STATUS | 
					
						
							|  |  |  |                     writer.close() | 
					
						
							|  |  |  |                     ctx.atari_streams = None | 
					
						
							|  |  |  |                 except CancelledError: | 
					
						
							|  |  |  |                     logger.debug("Connection Cancelled, Reconnecting") | 
					
						
							|  |  |  |                     error_status = CONNECTION_RESET_STATUS | 
					
						
							|  |  |  |                     writer.close() | 
					
						
							|  |  |  |                     ctx.atari_streams = None | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  |                 except Exception as e: | 
					
						
							|  |  |  |                     print("unknown exception " + e) | 
					
						
							|  |  |  |                     raise | 
					
						
							|  |  |  |                 if ctx.atari_status == CONNECTION_TENTATIVE_STATUS: | 
					
						
							|  |  |  |                     if not error_status: | 
					
						
							|  |  |  |                         logger.info("Successfully Connected to 2600") | 
					
						
							|  |  |  |                         ctx.atari_status = CONNECTION_CONNECTED_STATUS | 
					
						
							|  |  |  |                         ctx.checked_locations_sent = False | 
					
						
							|  |  |  |                         send_ap_foreign_items(ctx) | 
					
						
							|  |  |  |                         send_checked_locations_if_needed(ctx) | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}" | 
					
						
							|  |  |  |                 elif error_status: | 
					
						
							|  |  |  |                     ctx.atari_status = error_status | 
					
						
							|  |  |  |                     logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates") | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     port = ctx.lua_connector_port + ctx.port_offset | 
					
						
							|  |  |  |                     logger.debug(f"Attempting to connect to 2600 on port {port}") | 
					
						
							|  |  |  |                     print(f"Attempting to connect to 2600 on port {port}") | 
					
						
							|  |  |  |                     ctx.atari_streams = await asyncio.wait_for( | 
					
						
							|  |  |  |                         asyncio.open_connection("localhost", | 
					
						
							|  |  |  |                                                 port), | 
					
						
							| 
									
										
										
										
											2023-05-20 14:40:51 +02:00
										 |  |  |                         timeout=10) | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |                     ctx.atari_status = CONNECTION_TENTATIVE_STATUS | 
					
						
							|  |  |  |                 except TimeoutError: | 
					
						
							|  |  |  |                     logger.debug("Connection Timed Out, Trying Again") | 
					
						
							|  |  |  |                     ctx.atari_status = CONNECTION_TIMING_OUT_STATUS | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 except ConnectionRefusedError: | 
					
						
							|  |  |  |                     logger.debug("Connection Refused, Trying Again") | 
					
						
							|  |  |  |                     ctx.atari_status = CONNECTION_REFUSED_STATUS | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 except CancelledError: | 
					
						
							|  |  |  |                     pass | 
					
						
							|  |  |  |         except CancelledError: | 
					
						
							|  |  |  |             pass | 
					
						
							|  |  |  |     print("exiting atari sync task") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def run_game(romfile): | 
					
						
							| 
									
										
										
										
											2024-06-10 15:42:01 -07:00
										 |  |  |     auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True) | 
					
						
							|  |  |  |     rom_args = Utils.get_settings()["adventure_options"].get("rom_args") | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |     if auto_start is True: | 
					
						
							|  |  |  |         import webbrowser | 
					
						
							|  |  |  |         webbrowser.open(romfile) | 
					
						
							|  |  |  |     elif os.path.isfile(auto_start): | 
					
						
							|  |  |  |         open_args = [auto_start, romfile] | 
					
						
							|  |  |  |         if rom_args is not None: | 
					
						
							|  |  |  |             open_args.insert(1, rom_args) | 
					
						
							|  |  |  |         subprocess.Popen(open_args, | 
					
						
							|  |  |  |                          stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async def patch_and_run_game(patch_file, ctx): | 
					
						
							|  |  |  |     base_name = os.path.splitext(patch_file)[0] | 
					
						
							|  |  |  |     comp_path = base_name + '.a26' | 
					
						
							|  |  |  |     try: | 
					
						
							|  |  |  |         base_rom = AdventureDeltaPatch.get_source_data() | 
					
						
							|  |  |  |     except Exception as msg: | 
					
						
							|  |  |  |         logger.info(msg, extra={'compact_gui': True}) | 
					
						
							|  |  |  |         ctx.gui_error('Error', msg) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-04 10:32:03 -07:00
										 |  |  |     with open(Utils.local_path("data", "adventure_basepatch.bsdiff4"), "rb") as file: | 
					
						
							| 
									
										
										
										
											2023-03-22 07:25:55 -07:00
										 |  |  |         basepatch = bytes(file.read()) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     base_patched_rom_data = bsdiff4.patch(base_rom, basepatch) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with zipfile.ZipFile(patch_file, 'r') as patch_archive: | 
					
						
							|  |  |  |         if not AdventureDeltaPatch.check_version(patch_archive): | 
					
						
							|  |  |  |             logger.error("apadvn version doesn't match this client.  Make sure your generator and client are the same") | 
					
						
							|  |  |  |             raise Exception("apadvn version doesn't match this client.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive) | 
					
						
							|  |  |  |         ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive) | 
					
						
							|  |  |  |         ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive) | 
					
						
							|  |  |  |         ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive) | 
					
						
							|  |  |  |         ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive) | 
					
						
							|  |  |  |         ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive) | 
					
						
							|  |  |  |         ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive) | 
					
						
							|  |  |  |         ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive) | 
					
						
							|  |  |  |         ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive) | 
					
						
							|  |  |  |         ctx.auth = ctx.player_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas) | 
					
						
							|  |  |  |     rom_hash = hashlib.sha256() | 
					
						
							|  |  |  |     rom_hash.update(patched_rom_data) | 
					
						
							|  |  |  |     ctx.rom_hash = rom_hash.hexdigest() | 
					
						
							|  |  |  |     ctx.port_offset = patched_rom_data[connector_port_offset] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(comp_path, "wb") as patched_rom_file: | 
					
						
							|  |  |  |         patched_rom_file.write(patched_rom_data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async_start(run_game(comp_path)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == '__main__': | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Utils.init_logging("AdventureClient") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def main(): | 
					
						
							|  |  |  |         parser = get_base_parser() | 
					
						
							|  |  |  |         parser.add_argument('patch_file', default="", type=str, nargs="?", | 
					
						
							|  |  |  |                             help='Path to an ADVNTURE.BIN rom file') | 
					
						
							|  |  |  |         parser.add_argument('port', default=17242, type=int, nargs="?", | 
					
						
							|  |  |  |                             help='port for adventure_connector connection') | 
					
						
							|  |  |  |         args = parser.parse_args() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ctx = AdventureContext(args.connect, args.password) | 
					
						
							|  |  |  |         ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") | 
					
						
							|  |  |  |         if gui_enabled: | 
					
						
							|  |  |  |             ctx.run_gui() | 
					
						
							|  |  |  |         ctx.run_cli() | 
					
						
							|  |  |  |         ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if args.patch_file: | 
					
						
							|  |  |  |             ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower() | 
					
						
							|  |  |  |             if ext == "apadvn": | 
					
						
							|  |  |  |                 logger.info("apadvn file supplied, beginning patching process...") | 
					
						
							|  |  |  |                 async_start(patch_and_run_game(args.patch_file, ctx)) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 logger.warning(f"Unknown patch file extension {ext}") | 
					
						
							|  |  |  |         if args.port is int: | 
					
						
							|  |  |  |             ctx.lua_connector_port = args.port | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await ctx.exit_event.wait() | 
					
						
							|  |  |  |         ctx.server_address = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await ctx.shutdown() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ctx.atari_sync_task: | 
					
						
							|  |  |  |             await ctx.atari_sync_task | 
					
						
							|  |  |  |             print("finished atari_sync_task (main)") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     import colorama | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     colorama.init() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     asyncio.run(main()) | 
					
						
							|  |  |  |     colorama.deinit() |