import json import typing from websockets import WebSocketServerProtocol from . import TrackerConsts as Consts from .TrackerConsts import EntranceCoord from .LADXR.entranceInfo import ENTRANCE_INFO class Entrance: outdoor_room: int indoor_map: int indoor_address: int name: str other_side_name: str = None changed: bool = False known_to_server: bool = False def __init__(self, outdoor: int, indoor: int, name: str, indoor_address: int=None): self.outdoor_room = outdoor self.indoor_map = indoor self.indoor_address = indoor_address self.name = name def map(self, other_side: str, known_to_server: bool = False): if other_side != self.other_side_name: self.changed = True self.known_to_server = known_to_server self.other_side_name = other_side class GpsTracker: room: int = None last_room: int = None last_different_room: int = None room_same_for: int = 0 room_changed: bool = False screen_x: int = 0 screen_y: int = 0 spawn_x: int = 0 spawn_y: int = 0 indoors: int = None indoors_changed: bool = False spawn_map: int = None spawn_room: int = None spawn_changed: bool = False spawn_same_for: int = 0 entrance_mapping: typing.Dict[str, str] = None entrances_by_name: typing.Dict[str, Entrance] = {} needs_found_entrances: bool = False needs_slot_data: bool = True def __init__(self, gameboy) -> None: self.gameboy = gameboy self.gameboy.set_location_range( Consts.link_motion_state, Consts.transition_sequence - Consts.link_motion_state + 1, [Consts.transition_state] ) async def read_byte(self, b: int): return (await self.gameboy.read_memory_cache([b]))[b] def load_slot_data(self, slot_data: typing.Dict[str, typing.Any]): if 'entrance_mapping' not in slot_data: return # We need to know how entrances were mapped at generation before we can autotrack them self.entrance_mapping = {} # Convert to upstream's newer format for outside, inside in slot_data['entrance_mapping'].items(): new_inside = f"{inside}:inside" self.entrance_mapping[outside] = new_inside self.entrance_mapping[new_inside] = outside self.entrances_by_name = {} for name, info in ENTRANCE_INFO.items(): alternate_address = ( Consts.entrance_address_overrides[info.target] if info.target in Consts.entrance_address_overrides else None ) entrance = Entrance(info.room, info.target, name, alternate_address) self.entrances_by_name[name] = entrance inside_entrance = Entrance(info.target, info.room, f"{name}:inside", alternate_address) self.entrances_by_name[f"{name}:inside"] = inside_entrance self.needs_slot_data = False self.needs_found_entrances = True async def read_location(self): # We need to wait for screen transitions to finish transition_state = await self.read_byte(Consts.transition_state) transition_target_x = await self.read_byte(Consts.transition_target_x) transition_target_y = await self.read_byte(Consts.transition_target_y) transition_scroll_x = await self.read_byte(Consts.transition_scroll_x) transition_scroll_y = await self.read_byte(Consts.transition_scroll_y) transition_sequence = await self.read_byte(Consts.transition_sequence) motion_state = await self.read_byte(Consts.link_motion_state) if (transition_state != 0 or transition_target_x != transition_scroll_x or transition_target_y != transition_scroll_y or transition_sequence != 0x04): return indoors = await self.read_byte(Consts.indoor_flag) if indoors != self.indoors and self.indoors != None: self.indoors_changed = True self.indoors = indoors # We use the spawn point to know which entrance was most recently entered spawn_map = await self.read_byte(Consts.spawn_map) map_digit = Consts.map_map[spawn_map] << 8 if self.spawn_map else 0 spawn_room = await self.read_byte(Consts.spawn_room) + map_digit spawn_x = await self.read_byte(Consts.spawn_x) spawn_y = await self.read_byte(Consts.spawn_y) # The spawn point needs to be settled before we can trust location data if ((spawn_room != self.spawn_room and self.spawn_room != None) or (spawn_map != self.spawn_map and self.spawn_map != None) or (spawn_x != self.spawn_x and self.spawn_x != None) or (spawn_y != self.spawn_y and self.spawn_y != None)): self.spawn_changed = True self.spawn_same_for = 0 else: self.spawn_same_for += 1 self.spawn_map = spawn_map self.spawn_room = spawn_room self.spawn_x = spawn_x self.spawn_y = spawn_y # Spawn point is preferred, but doesn't work for the sidescroller entrances # Those can be addressed by keeping track of which room we're in # Also used to validate that we came from the right room for what the spawn point is mapped to map_id = await self.read_byte(Consts.map_id) if map_id not in Consts.map_map: print(f'Unknown map ID {hex(map_id)}') return map_digit = Consts.map_map[map_id] << 8 if indoors else 0 self.last_room = self.room self.room = await self.read_byte(Consts.room) + map_digit # Again, the room needs to settle before we can trust location data if self.last_room != self.room: self.room_same_for = 0 self.room_changed = True self.last_different_room = self.last_room else: self.room_same_for += 1 # Only update Link's location when he's not in the air to avoid weirdness if motion_state in [0, 1]: coords = await self.read_byte(Consts.screen_coord) self.screen_x = coords & 0x0F self.screen_y = (coords & 0xF0) >> 4 async def read_entrances(self): if not self.last_different_room or not self.entrance_mapping: return if self.spawn_changed and self.spawn_same_for > 0 and self.room_same_for > 0: # Use the spawn location, last room, and entrance mapping at generation to map the right entrance # A bit overkill for simple ER, but necessary for upstream's advanced ER spawn_coord = EntranceCoord(None, self.spawn_room, self.spawn_x, self.spawn_y) if str(spawn_coord) in Consts.entrance_lookup: valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room} dest_entrance = Consts.entrance_lookup[str(spawn_coord)].name source_entrance = [ x for x in self.entrance_mapping if self.entrance_mapping[x] == dest_entrance and x in valid_sources ] if source_entrance: self.entrances_by_name[source_entrance[0]].map(dest_entrance) self.spawn_changed = False elif self.room_changed and self.room_same_for > 0: # Check for the stupid sidescroller rooms that don't set your spawn point if self.last_different_room in Consts.sidescroller_rooms: source_entrance = Consts.sidescroller_rooms[self.last_different_room] if source_entrance in self.entrance_mapping: dest_entrance = self.entrance_mapping[source_entrance] expected_room = self.entrances_by_name[dest_entrance].outdoor_room if dest_entrance.endswith(":indoor"): expected_room = self.entrances_by_name[dest_entrance].indoor_map if expected_room == self.room: self.entrances_by_name[source_entrance].map(dest_entrance) if self.room in Consts.sidescroller_rooms: valid_sources = {x.name for x in Consts.entrance_coords if x.room == self.last_different_room} dest_entrance = Consts.sidescroller_rooms[self.room] source_entrance = [ x for x in self.entrance_mapping if self.entrance_mapping[x] == dest_entrance and x in valid_sources ] if source_entrance: self.entrances_by_name[source_entrance[0]].map(dest_entrance) self.room_changed = False last_location_message = {} async def send_location(self, socket: WebSocketServerProtocol) -> None: if self.room is None or self.room_same_for < 1: return message = { "type":"location", "refresh": True, "room": f'0x{self.room:02X}', "x": self.screen_x, "y": self.screen_y, "drawFine": True, } if message != self.last_location_message: self.last_location_message = message await socket.send(json.dumps(message)) async def send_entrances(self, socket: WebSocketServerProtocol, diff: bool=True) -> typing.Dict[str, str]: if not self.entrance_mapping: return new_entrances = [x for x in self.entrances_by_name.values() if x.changed or (not diff and x.other_side_name)] if not new_entrances: return message = { "type":"entrance", "refresh": True, "diff": True, "entranceMap": {}, } for entrance in new_entrances: message['entranceMap'][entrance.name] = entrance.other_side_name entrance.changed = False await socket.send(json.dumps(message)) new_to_server = { entrance.name: entrance.other_side_name for entrance in new_entrances if not entrance.known_to_server } return new_to_server def receive_found_entrances(self, found_entrances: typing.Dict[str, str]): if not found_entrances: return for entrance, destination in found_entrances.items(): if entrance in self.entrances_by_name: self.entrances_by_name[entrance].map(destination, known_to_server=True)