| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  | from worlds.ladx.LADXR.checkMetadata import checkMetadataTable | 
					
						
							|  |  |  | import json | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import websockets | 
					
						
							|  |  |  | import asyncio | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | logger = logging.getLogger("Tracker") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # kbranch you're a hero | 
					
						
							|  |  |  | # https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py | 
					
						
							|  |  |  | class Check: | 
					
						
							|  |  |  |     def __init__(self, id, address, mask, alternateAddress=None): | 
					
						
							|  |  |  |         self.id = id | 
					
						
							|  |  |  |         self.address = address | 
					
						
							|  |  |  |         self.alternateAddress = alternateAddress | 
					
						
							|  |  |  |         self.mask = mask | 
					
						
							|  |  |  |         self.value = None | 
					
						
							|  |  |  |         self.diff = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def set(self, bytes): | 
					
						
							|  |  |  |         oldValue = self.value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.value = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for byte in bytes: | 
					
						
							|  |  |  |             maskedByte = byte | 
					
						
							|  |  |  |             if self.mask: | 
					
						
							|  |  |  |                 maskedByte &= self.mask | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             self.value |= int(maskedByte > 0) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if oldValue != self.value: | 
					
						
							|  |  |  |             self.diff += self.value - (oldValue or 0) | 
					
						
							|  |  |  | # Todo: unify this with existing item tables? | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class LocationTracker: | 
					
						
							|  |  |  |     all_checks = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, gameboy): | 
					
						
							|  |  |  |         self.gameboy = gameboy | 
					
						
							|  |  |  |         maskOverrides = { | 
					
						
							|  |  |  |             '0x106': 0x20, | 
					
						
							|  |  |  |             '0x12B': 0x20, | 
					
						
							|  |  |  |             '0x15A': 0x20, | 
					
						
							|  |  |  |             '0x166': 0x20, | 
					
						
							|  |  |  |             '0x185': 0x20, | 
					
						
							|  |  |  |             '0x1E4': 0x20, | 
					
						
							|  |  |  |             '0x1BC': 0x20, | 
					
						
							|  |  |  |             '0x1E0': 0x20, | 
					
						
							|  |  |  |             '0x1E1': 0x20, | 
					
						
							|  |  |  |             '0x1E2': 0x20, | 
					
						
							|  |  |  |             '0x223': 0x20, | 
					
						
							|  |  |  |             '0x234': 0x20, | 
					
						
							|  |  |  |             '0x2A3': 0x20, | 
					
						
							|  |  |  |             '0x2FD': 0x20, | 
					
						
							|  |  |  |             '0x2A7': 0x20, | 
					
						
							|  |  |  |             '0x1F5': 0x06, | 
					
						
							|  |  |  |             '0x301-0': 0x10, | 
					
						
							|  |  |  |             '0x301-1': 0x10, | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         addressOverrides = { | 
					
						
							|  |  |  |             '0x30A-Owl': 0xDDEA, | 
					
						
							|  |  |  |             '0x30F-Owl': 0xDDEF, | 
					
						
							|  |  |  |             '0x308-Owl': 0xDDE8, | 
					
						
							|  |  |  |             '0x302': 0xDDE2, | 
					
						
							|  |  |  |             '0x306': 0xDDE6, | 
					
						
							|  |  |  |             '0x307': 0xDDE7, | 
					
						
							|  |  |  |             '0x308': 0xDDE8, | 
					
						
							|  |  |  |             '0x30F': 0xDDEF, | 
					
						
							|  |  |  |             '0x311': 0xDDF1, | 
					
						
							|  |  |  |             '0x314': 0xDDF4, | 
					
						
							|  |  |  |             '0x1F5': 0xDB7D, | 
					
						
							|  |  |  |             '0x301-0': 0xDDE1, | 
					
						
							|  |  |  |             '0x301-1': 0xDDE1, | 
					
						
							|  |  |  |             '0x223': 0xDA2E, | 
					
						
							|  |  |  |             '0x169': 0xD97C, | 
					
						
							|  |  |  |             '0x2A7': 0xD800 + 0x2A1 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         alternateAddresses = { | 
					
						
							|  |  |  |             '0x0F2': 0xD8B2, | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         blacklist = {'None', '0x2A1-2'} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC) | 
					
						
							|  |  |  |         # after beating evil eagile in D6, 0x1BC is now 0xAC (other things may have happened in between) | 
					
						
							|  |  |  |         # entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set | 
					
						
							|  |  |  |         lowest_check = 0xffff | 
					
						
							|  |  |  |         highest_check = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for check_id in [x for x in checkMetadataTable if x not in blacklist]: | 
					
						
							|  |  |  |             room = check_id.split('-')[0] | 
					
						
							|  |  |  |             mask = 0x10 | 
					
						
							|  |  |  |             address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int( | 
					
						
							|  |  |  |                 room, 16) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if 'Trade' in check_id or 'Owl' in check_id: | 
					
						
							|  |  |  |                 mask = 0x20 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if check_id in maskOverrides: | 
					
						
							|  |  |  |                 mask = maskOverrides[check_id] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             lowest_check = min(lowest_check, address) | 
					
						
							|  |  |  |             highest_check = max(highest_check, address) | 
					
						
							|  |  |  |             if check_id in alternateAddresses: | 
					
						
							|  |  |  |                 lowest_check = min(lowest_check, alternateAddresses[check_id]) | 
					
						
							|  |  |  |                 highest_check = max( | 
					
						
							|  |  |  |                     highest_check, alternateAddresses[check_id]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             check = Check(check_id, address, mask, | 
					
						
							|  |  |  |                           alternateAddresses[check_id] if check_id in alternateAddresses else None) | 
					
						
							|  |  |  |             if check_id == '0x2A3': | 
					
						
							|  |  |  |                 self.start_check = check | 
					
						
							|  |  |  |             self.all_checks.append(check) | 
					
						
							|  |  |  |         self.remaining_checks = [check for check in self.all_checks] | 
					
						
							|  |  |  |         self.gameboy.set_cache_limits( | 
					
						
							|  |  |  |             lowest_check, highest_check - lowest_check + 1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def has_start_item(self): | 
					
						
							|  |  |  |         return self.start_check not in self.remaining_checks | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def readChecks(self, cb): | 
					
						
							|  |  |  |         new_checks = [] | 
					
						
							|  |  |  |         for check in self.remaining_checks: | 
					
						
							|  |  |  |             addresses = [check.address] | 
					
						
							|  |  |  |             if check.alternateAddress: | 
					
						
							|  |  |  |                 addresses.append(check.alternateAddress) | 
					
						
							|  |  |  |             bytes = await self.gameboy.read_memory_cache(addresses) | 
					
						
							|  |  |  |             if not bytes: | 
					
						
							|  |  |  |                 return False | 
					
						
							|  |  |  |             check.set(list(bytes.values())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if check.value: | 
					
						
							|  |  |  |                 self.remaining_checks.remove(check) | 
					
						
							|  |  |  |                 new_checks.append(check) | 
					
						
							|  |  |  |         if new_checks: | 
					
						
							|  |  |  |             cb(new_checks) | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class MagpieBridge: | 
					
						
							|  |  |  |     port = 17026 | 
					
						
							|  |  |  |     server = None | 
					
						
							|  |  |  |     checks = None | 
					
						
							|  |  |  |     item_tracker = None | 
					
						
							|  |  |  |     ws = None | 
					
						
							| 
									
										
										
										
											2023-04-14 21:47:36 -07:00
										 |  |  |     features = [] | 
					
						
							| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  |     async def handler(self, websocket): | 
					
						
							|  |  |  |         self.ws = websocket | 
					
						
							|  |  |  |         while True: | 
					
						
							|  |  |  |             message = json.loads(await websocket.recv()) | 
					
						
							|  |  |  |             if message["type"] == "handshake": | 
					
						
							|  |  |  |                 logger.info( | 
					
						
							|  |  |  |                     f"Connected, supported features: {message['features']}") | 
					
						
							| 
									
										
										
										
											2023-04-14 21:47:36 -07:00
										 |  |  |                 self.features = message["features"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if message["type"] in ("handshake", "sendFull"): | 
					
						
							|  |  |  |                 if "items" in self.features: | 
					
						
							| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  |                     await self.send_all_inventory() | 
					
						
							| 
									
										
										
										
											2023-04-14 21:47:36 -07:00
										 |  |  |                 if "checks" in self.features: | 
					
						
							| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  |                     await self.send_all_checks() | 
					
						
							| 
									
										
										
										
											2023-03-29 05:56:10 -07:00
										 |  |  |     # Translate renamed IDs back to LADXR IDs | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def fixup_id(the_id): | 
					
						
							|  |  |  |         if the_id == "0x2A1": | 
					
						
							|  |  |  |             return "0x2A1-0" | 
					
						
							|  |  |  |         if the_id == "0x2A7": | 
					
						
							|  |  |  |             return "0x2A1-1" | 
					
						
							|  |  |  |         return the_id | 
					
						
							| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  | 
 | 
					
						
							|  |  |  |     async def send_all_checks(self): | 
					
						
							|  |  |  |         while self.checks == None: | 
					
						
							|  |  |  |             await asyncio.sleep(0.1) | 
					
						
							|  |  |  |         logger.info("sending all checks to magpie") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         message = { | 
					
						
							|  |  |  |             "type": "check", | 
					
						
							|  |  |  |             "refresh":  True, | 
					
						
							|  |  |  |             "version": "1.0", | 
					
						
							|  |  |  |             "diff": False, | 
					
						
							| 
									
										
										
										
											2023-03-29 05:56:10 -07:00
										 |  |  |             "checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks] | 
					
						
							| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await self.ws.send(json.dumps(message)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def send_new_checks(self, checks): | 
					
						
							|  |  |  |         if not self.ws: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         logger.debug("Sending new {checks} to magpie") | 
					
						
							|  |  |  |         message = { | 
					
						
							|  |  |  |             "type": "check", | 
					
						
							|  |  |  |             "refresh": True, | 
					
						
							|  |  |  |             "version": "1.0", | 
					
						
							|  |  |  |             "diff": True, | 
					
						
							| 
									
										
										
										
											2023-03-29 05:56:10 -07:00
										 |  |  |             "checks": [{"id": self.fixup_id(check), "checked": True} for check in checks] | 
					
						
							| 
									
										
										
										
											2023-03-21 01:26:03 +09:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await self.ws.send(json.dumps(message)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def send_all_inventory(self): | 
					
						
							|  |  |  |         logger.info("Sending inventory to magpie") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         while self.item_tracker == None: | 
					
						
							|  |  |  |             await asyncio.sleep(0.1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         await self.item_tracker.sendItems(self.ws) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def send_inventory_diffs(self): | 
					
						
							|  |  |  |         if not self.ws: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         if not self.item_tracker: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         await self.item_tracker.sendItems(self.ws, diff=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def send_gps(self, gps): | 
					
						
							|  |  |  |         if not self.ws: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         await gps.send_location(self.ws) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def serve(self): | 
					
						
							|  |  |  |         async with websockets.serve(lambda w: self.handler(w), "", 17026, logger=logger): | 
					
						
							|  |  |  |             await asyncio.Future()  # run forever | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def set_checks(self, checks): | 
					
						
							|  |  |  |         self.checks = checks | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     async def set_item_tracker(self, item_tracker): | 
					
						
							|  |  |  |         stale_tracker = self.item_tracker != item_tracker | 
					
						
							|  |  |  |         self.item_tracker = item_tracker | 
					
						
							|  |  |  |         if stale_tracker: | 
					
						
							|  |  |  |             if self.ws: | 
					
						
							|  |  |  |                 await self.send_all_inventory() | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             await self.send_inventory_diffs() | 
					
						
							|  |  |  | 
 |