LADX: Autotracker improvements (#4445)

* Expand and validate the RAM cache

* Part way through location improvement

* Fixed location tracking

* Preliminary entrance tracking support

* Actually send entrance messages

* Store found entrances on the server

* Bit of cleanup

* Added rupee count, items linked to checks

* Send Magpie a handshAck

* Got my own version wrong

* Remove the Beta name

* Only send slot_data if there's something in it

* Ask the server for entrance updates

* Small fix to stabilize Link's location when changing rooms

* Oops, server storage is shared between worlds

* Deal with null responses from the server

* Added UNUSED_KEY item
This commit is contained in:
kbranch
2025-03-08 07:32:45 -05:00
committed by GitHub
parent 3e08acf381
commit 9c57976252
5 changed files with 752 additions and 110 deletions

View File

@@ -28,6 +28,7 @@ from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
from NetUtils import ClientStatus from NetUtils import ClientStatus
from worlds.ladx.Common import BASE_ID as LABaseID from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key
from worlds.ladx.ItemTracker import ItemTracker from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name from worlds.ladx.Locations import get_locations_to_id, meta_to_name
@@ -100,19 +101,23 @@ class LAClientConstants:
WRamCheckSize = 0x4 WRamCheckSize = 0x4
WRamSafetyValue = bytearray([0]*WRamCheckSize) WRamSafetyValue = bytearray([0]*WRamCheckSize)
wRamStart = 0xC000
hRamStart = 0xFF80
hRamSize = 0x80
MinGameplayValue = 0x06 MinGameplayValue = 0x06
MaxGameplayValue = 0x1A MaxGameplayValue = 0x1A
VictoryGameplayAndSub = 0x0102 VictoryGameplayAndSub = 0x0102
class RAGameboy(): class RAGameboy():
cache = [] cache = []
cache_start = 0
cache_size = 0
last_cache_read = None last_cache_read = None
socket = None socket = None
def __init__(self, address, port) -> None: def __init__(self, address, port) -> None:
self.cache_start = LAClientConstants.wRamStart
self.cache_size = LAClientConstants.hRamStart + LAClientConstants.hRamSize - LAClientConstants.wRamStart
self.address = address self.address = address
self.port = port self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -131,9 +136,14 @@ class RAGameboy():
async def get_retroarch_status(self): async def get_retroarch_status(self):
return await self.send_command("GET_STATUS") return await self.send_command("GET_STATUS")
def set_cache_limits(self, cache_start, cache_size): def set_checks_range(self, checks_start, checks_size):
self.cache_start = cache_start self.checks_start = checks_start
self.cache_size = cache_size self.checks_size = checks_size
def set_location_range(self, location_start, location_size, critical_addresses):
self.location_start = location_start
self.location_size = location_size
self.critical_location_addresses = critical_addresses
def send(self, b): def send(self, b):
if type(b) is str: if type(b) is str:
@@ -188,21 +198,57 @@ class RAGameboy():
if not await self.check_safe_gameplay(): if not await self.check_safe_gameplay():
return return
cache = [] attempts = 0
remaining_size = self.cache_size while True:
while remaining_size: # RA doesn't let us do an atomic read of a large enough block of RAM
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size) # Some bytes can't change in between reading location_block and hram_block
remaining_size -= len(block) location_block = await self.read_memory_block(self.location_start, self.location_size)
cache += block hram_block = await self.read_memory_block(LAClientConstants.hRamStart, LAClientConstants.hRamSize)
verification_block = await self.read_memory_block(self.location_start, self.location_size)
valid = True
for address in self.critical_location_addresses:
if location_block[address - self.location_start] != verification_block[address - self.location_start]:
valid = False
if valid:
break
attempts += 1
# Shouldn't really happen, but keep it from choking
if attempts > 5:
return
checks_block = await self.read_memory_block(self.checks_start, self.checks_size)
if not await self.check_safe_gameplay(): if not await self.check_safe_gameplay():
return return
self.cache = cache self.cache = bytearray(self.cache_size)
start = self.checks_start - self.cache_start
self.cache[start:start + len(checks_block)] = checks_block
start = self.location_start - self.cache_start
self.cache[start:start + len(location_block)] = location_block
start = LAClientConstants.hRamStart - self.cache_start
self.cache[start:start + len(hram_block)] = hram_block
self.last_cache_read = time.time() self.last_cache_read = time.time()
async def read_memory_block(self, address: int, size: int):
block = bytearray()
remaining_size = size
while remaining_size:
chunk = await self.async_read_memory(address + len(block), remaining_size)
remaining_size -= len(chunk)
block += chunk
return block
async def read_memory_cache(self, addresses): async def read_memory_cache(self, addresses):
# TODO: can we just update once per frame?
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time(): if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
await self.update_cache() await self.update_cache()
if not self.cache: if not self.cache:
@@ -359,11 +405,12 @@ class LinksAwakeningClient():
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode() auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
self.auth = auth self.auth = auth
async def wait_and_init_tracker(self): async def wait_and_init_tracker(self, magpie: MagpieBridge):
await self.wait_for_game_ready() await self.wait_for_game_ready()
self.tracker = LocationTracker(self.gameboy) self.tracker = LocationTracker(self.gameboy)
self.item_tracker = ItemTracker(self.gameboy) self.item_tracker = ItemTracker(self.gameboy)
self.gps_tracker = GpsTracker(self.gameboy) self.gps_tracker = GpsTracker(self.gameboy)
magpie.gps_tracker = self.gps_tracker
async def recved_item_from_ap(self, item_id, from_player, next_index): async def recved_item_from_ap(self, item_id, from_player, next_index):
# Don't allow getting an item until you've got your first check # Don't allow getting an item until you've got your first check
@@ -405,9 +452,11 @@ class LinksAwakeningClient():
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1 return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
async def main_tick(self, item_get_cb, win_cb, deathlink_cb): async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
await self.gameboy.update_cache()
await self.tracker.readChecks(item_get_cb) await self.tracker.readChecks(item_get_cb)
await self.item_tracker.readItems() await self.item_tracker.readItems()
await self.gps_tracker.read_location() await self.gps_tracker.read_location()
await self.gps_tracker.read_entrances()
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth] current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
if self.deathlink_debounce and current_health != 0: if self.deathlink_debounce and current_health != 0:
@@ -465,6 +514,10 @@ class LinksAwakeningContext(CommonContext):
magpie_task = None magpie_task = None
won = False won = False
@property
def slot_storage_key(self):
return f"{self.slot_info[self.slot].name}_{storage_key}"
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient() self.client = LinksAwakeningClient()
self.slot_data = {} self.slot_data = {}
@@ -507,7 +560,19 @@ class LinksAwakeningContext(CommonContext):
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_checks(self): async def send_checks(self):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}] message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
await self.send_msgs(message)
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
# Store the entrances we find on the server for future sessions
message = [{
"cmd": "Set",
"key": self.slot_storage_key,
"default": {},
"want_reply": False,
"operations": [{"operation": "update", "value": entrances}],
}]
await self.send_msgs(message) await self.send_msgs(message)
had_invalid_slot_data = None had_invalid_slot_data = None
@@ -537,6 +602,12 @@ class LinksAwakeningContext(CommonContext):
await self.send_msgs(message) await self.send_msgs(message)
self.won = True self.won = True
async def request_found_entrances(self):
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
# Ask for updates so that players can co-op entrances in a seed
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None: async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if self.ENABLE_DEATHLINK: if self.ENABLE_DEATHLINK:
self.client.pending_deathlink = True self.client.pending_deathlink = True
@@ -577,6 +648,12 @@ class LinksAwakeningContext(CommonContext):
for index, item in enumerate(args["items"], start=args["index"]): for index, item in enumerate(args["items"], start=args["index"]):
self.client.recvd_checks[index] = item self.client.recvd_checks[index] = item
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
if cmd == "SetReply" and self.magpie_enabled and args["key"] == self.slot_storage_key:
self.client.gps_tracker.receive_found_entrances(args["value"])
async def sync(self): async def sync(self):
sync_msg = [{'cmd': 'Sync'}] sync_msg = [{'cmd': 'Sync'}]
await self.send_msgs(sync_msg) await self.send_msgs(sync_msg)
@@ -589,6 +666,12 @@ class LinksAwakeningContext(CommonContext):
checkMetadataTable[check.id])] for check in ladxr_checks] checkMetadataTable[check.id])] for check in ladxr_checks]
self.new_checks(checks, [check.id for check in ladxr_checks]) self.new_checks(checks, [check.id for check in ladxr_checks])
for check in ladxr_checks:
if check.value and check.linkedItem:
linkedItem = check.linkedItem
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
async def victory(): async def victory():
await self.send_victory() await self.send_victory()
@@ -622,12 +705,20 @@ class LinksAwakeningContext(CommonContext):
if not self.client.recvd_checks: if not self.client.recvd_checks:
await self.sync() await self.sync()
await self.client.wait_and_init_tracker() await self.client.wait_and_init_tracker(self.magpie)
min_tick_duration = 0.1
last_tick = time.time()
while True: while True:
await self.client.main_tick(on_item_get, victory, deathlink) await self.client.main_tick(on_item_get, victory, deathlink)
await asyncio.sleep(0.1)
now = time.time() now = time.time()
tick_duration = now - last_tick
sleep_duration = max(min_tick_duration - tick_duration, 0)
await asyncio.sleep(sleep_duration)
last_tick = now
if self.last_resend + 5.0 < now: if self.last_resend + 5.0 < now:
self.last_resend = now self.last_resend = now
await self.send_checks() await self.send_checks()
@@ -635,8 +726,15 @@ class LinksAwakeningContext(CommonContext):
try: try:
self.magpie.set_checks(self.client.tracker.all_checks) self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
self.magpie.slot_data = self.slot_data self.magpie.slot_data = self.slot_data
if self.client.gps_tracker.needs_found_entrances:
await self.request_found_entrances()
self.client.gps_tracker.needs_found_entrances = False
new_entrances = await self.magpie.send_gps(self.client.gps_tracker)
if new_entrances:
await self.send_new_entrances(new_entrances)
except Exception: except Exception:
# Don't let magpie errors take out the client # Don't let magpie errors take out the client
pass pass

View File

@@ -1,92 +1,266 @@
import json import json
roomAddress = 0xFFF6 import typing
mapIdAddress = 0xFFF7 from websockets import WebSocketServerProtocol
indoorFlagAddress = 0xDBA5
entranceRoomOffset = 0xD800
screenCoordAddress = 0xFFFA
mapMap = { from . import TrackerConsts as Consts
0x00: 0x01, from .TrackerConsts import EntranceCoord
0x01: 0x01, from .LADXR.entranceInfo import ENTRANCE_INFO
0x02: 0x01,
0x03: 0x01, class Entrance:
0x04: 0x01, outdoor_room: int
0x05: 0x01, indoor_map: int
0x06: 0x02, indoor_address: int
0x07: 0x02, name: str
0x08: 0x02, other_side_name: str = None
0x09: 0x02, changed: bool = False
0x0A: 0x02, known_to_server: bool = False
0x0B: 0x02,
0x0C: 0x02, def __init__(self, outdoor: int, indoor: int, name: str, indoor_address: int=None):
0x0D: 0x02, self.outdoor_room = outdoor
0x0E: 0x02, self.indoor_map = indoor
0x0F: 0x02, self.indoor_address = indoor_address
0x10: 0x02, self.name = name
0x11: 0x02,
0x12: 0x02, def map(self, other_side: str, known_to_server: bool = False):
0x13: 0x02, if other_side != self.other_side_name:
0x14: 0x02, self.changed = True
0x15: 0x02, self.known_to_server = known_to_server
0x16: 0x02,
0x17: 0x02, self.other_side_name = other_side
0x18: 0x02,
0x19: 0x02,
0x1D: 0x01,
0x1E: 0x01,
0x1F: 0x01,
0xFF: 0x03,
}
class GpsTracker: class GpsTracker:
room = None room: int = None
location_changed = False last_room: int = None
screenX = 0 last_different_room: int = None
screenY = 0 room_same_for: int = 0
indoors = None 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: def __init__(self, gameboy) -> None:
self.gameboy = gameboy self.gameboy = gameboy
async def read_byte(self, b): self.gameboy.set_location_range(
return (await self.gameboy.async_read_memory(b))[0] 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): async def read_location(self):
indoors = await self.read_byte(indoorFlagAddress) # 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: if indoors != self.indoors and self.indoors != None:
self.indoorsChanged = True self.indoors_changed = True
self.indoors = indoors self.indoors = indoors
mapId = await self.read_byte(mapIdAddress) # We use the spawn point to know which entrance was most recently entered
if mapId not in mapMap: spawn_map = await self.read_byte(Consts.spawn_map)
print(f'Unknown map ID {hex(mapId)}') 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 return
mapDigit = mapMap[mapId] << 8 if indoors else 0 map_digit = Consts.map_map[map_id] << 8 if indoors else 0
last_room = self.room self.last_room = self.room
self.room = await self.read_byte(roomAddress) + mapDigit self.room = await self.read_byte(Consts.room) + map_digit
coords = await self.read_byte(screenCoordAddress) # Again, the room needs to settle before we can trust location data
self.screenX = coords & 0x0F if self.last_room != self.room:
self.screenY = (coords & 0xF0) >> 4 self.room_same_for = 0
self.room_changed = True
self.last_different_room = self.last_room
else:
self.room_same_for += 1
if (self.room != last_room): # Only update Link's location when he's not in the air to avoid weirdness
self.location_changed = True 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
last_message = {} async def read_entrances(self):
async def send_location(self, socket, diff=False): if not self.last_different_room or not self.entrance_mapping:
if self.room is None:
return 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 = { message = {
"type":"location", "type":"location",
"refresh": True, "refresh": True,
"version":"1.0",
"room": f'0x{self.room:02X}', "room": f'0x{self.room:02X}',
"x": self.screenX, "x": self.screen_x,
"y": self.screenY, "y": self.screen_y,
"drawFine": True,
} }
if message != self.last_message:
self.last_message = message if message != self.last_location_message:
self.last_location_message = message
await socket.send(json.dumps(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)

View File

@@ -1,12 +1,16 @@
import json import json
gameStateAddress = 0xDB95
validGameStates = {0x0B, 0x0C}
gameStateResetThreshold = 0x06
inventorySlotCount = 16 inventorySlotCount = 16
inventoryStartAddress = 0xDB00 inventoryStartAddress = 0xDB00
inventoryEndAddress = inventoryStartAddress + inventorySlotCount inventoryEndAddress = inventoryStartAddress + inventorySlotCount
rupeesHigh = 0xDB5D
rupeesLow = 0xDB5E
addRupeesHigh = 0xDB8F
addRupeesLow = 0xDB90
removeRupeesHigh = 0xDB91
removeRupeesLow = 0xDB92
inventoryItemIds = { inventoryItemIds = {
0x02: 'BOMB', 0x02: 'BOMB',
0x05: 'BOW', 0x05: 'BOW',
@@ -98,10 +102,11 @@ dungeonItemOffsets = {
'STONE_BEAK{}': 2, 'STONE_BEAK{}': 2,
'NIGHTMARE_KEY{}': 3, 'NIGHTMARE_KEY{}': 3,
'KEY{}': 4, 'KEY{}': 4,
'UNUSED_KEY{}': 4,
} }
class Item: class Item:
def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None): def __init__(self, id, address, threshold=0, mask=None, increaseOnly=False, count=False, max=None, encodedCount=True):
self.id = id self.id = id
self.address = address self.address = address
self.threshold = threshold self.threshold = threshold
@@ -112,6 +117,7 @@ class Item:
self.rawValue = 0 self.rawValue = 0
self.diff = 0 self.diff = 0
self.max = max self.max = max
self.encodedCount = encodedCount
def set(self, byte, extra): def set(self, byte, extra):
oldValue = self.value oldValue = self.value
@@ -121,7 +127,7 @@ class Item:
if not self.count: if not self.count:
byte = int(byte > self.threshold) byte = int(byte > self.threshold)
else: elif self.encodedCount:
# LADX seems to store one decimal digit per nibble # LADX seems to store one decimal digit per nibble
byte = byte - (byte // 16 * 6) byte = byte - (byte // 16 * 6)
@@ -165,6 +171,7 @@ class ItemTracker:
Item('BOOMERANG', None), Item('BOOMERANG', None),
Item('TOADSTOOL', None), Item('TOADSTOOL', None),
Item('ROOSTER', None), Item('ROOSTER', None),
Item('RUPEE_COUNT', None, count=True, encodedCount=False),
Item('SWORD', 0xDB4E, count=True), Item('SWORD', 0xDB4E, count=True),
Item('POWER_BRACELET', 0xDB43, count=True), Item('POWER_BRACELET', 0xDB43, count=True),
Item('SHIELD', 0xDB44, count=True), Item('SHIELD', 0xDB44, count=True),
@@ -219,9 +226,9 @@ class ItemTracker:
self.itemDict = {item.id: item for item in self.items} self.itemDict = {item.id: item for item in self.items}
async def readItems(state): async def readItems(self):
extraItems = state.extraItems extraItems = self.extraItems
missingItems = {x for x in state.items if x.address == None} missingItems = {x for x in self.items if x.address == None and x.id != 'RUPEE_COUNT'}
# Add keys for opened key doors # Add keys for opened key doors
for i in range(len(dungeonKeyDoors)): for i in range(len(dungeonKeyDoors)):
@@ -230,16 +237,16 @@ class ItemTracker:
for address, masks in dungeonKeyDoors[i].items(): for address, masks in dungeonKeyDoors[i].items():
for mask in masks: for mask in masks:
value = await state.readRamByte(address) & mask value = await self.readRamByte(address) & mask
if value > 0: if value > 0:
extraItems[item] += 1 extraItems[item] += 1
# Main inventory items # Main inventory items
for i in range(inventoryStartAddress, inventoryEndAddress): for i in range(inventoryStartAddress, inventoryEndAddress):
value = await state.readRamByte(i) value = await self.readRamByte(i)
if value in inventoryItemIds: if value in inventoryItemIds:
item = state.itemDict[inventoryItemIds[value]] item = self.itemDict[inventoryItemIds[value]]
extra = extraItems[item.id] if item.id in extraItems else 0 extra = extraItems[item.id] if item.id in extraItems else 0
item.set(1, extra) item.set(1, extra)
missingItems.remove(item) missingItems.remove(item)
@@ -249,9 +256,21 @@ class ItemTracker:
item.set(0, extra) item.set(0, extra)
# All other items # All other items
for item in [x for x in state.items if x.address]: for item in [x for x in self.items if x.address]:
extra = extraItems[item.id] if item.id in extraItems else 0 extra = extraItems[item.id] if item.id in extraItems else 0
item.set(await state.readRamByte(item.address), extra) item.set(await self.readRamByte(item.address), extra)
# The current rupee count is BCD, but the add/remove values are not
currentRupees = self.calculateRupeeCount(await self.readRamByte(rupeesHigh), await self.readRamByte(rupeesLow))
addingRupees = (await self.readRamByte(addRupeesHigh) << 8) + await self.readRamByte(addRupeesLow)
removingRupees = (await self.readRamByte(removeRupeesHigh) << 8) + await self.readRamByte(removeRupeesLow)
self.itemDict['RUPEE_COUNT'].set(currentRupees + addingRupees - removingRupees, 0)
def calculateRupeeCount(self, high: int, low: int) -> int:
return (high - (high // 16 * 6)) * 100 + (low - (low // 16 * 6))
def setExtraItem(self, item: str, qty: int) -> None:
self.extraItems[item] = qty
async def sendItems(self, socket, diff=False): async def sendItems(self, socket, diff=False):
if not self.items: if not self.items:
@@ -259,7 +278,6 @@ class ItemTracker:
message = { message = {
"type":"item", "type":"item",
"refresh": True, "refresh": True,
"version":"1.0",
"diff": diff, "diff": diff,
"items": [], "items": [],
} }

View File

@@ -1,3 +1,6 @@
import typing
from worlds.ladx.GpsTracker import GpsTracker
from .LADXR.checkMetadata import checkMetadataTable from .LADXR.checkMetadata import checkMetadataTable
import json import json
import logging import logging
@@ -10,13 +13,14 @@ logger = logging.getLogger("Tracker")
# kbranch you're a hero # kbranch you're a hero
# https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py # https://github.com/kbranch/Magpie/blob/master/autotracking/checks.py
class Check: class Check:
def __init__(self, id, address, mask, alternateAddress=None): def __init__(self, id, address, mask, alternateAddress=None, linkedItem=None):
self.id = id self.id = id
self.address = address self.address = address
self.alternateAddress = alternateAddress self.alternateAddress = alternateAddress
self.mask = mask self.mask = mask
self.value = None self.value = None
self.diff = 0 self.diff = 0
self.linkedItem = linkedItem
def set(self, bytes): def set(self, bytes):
oldValue = self.value oldValue = self.value
@@ -86,6 +90,27 @@ class LocationTracker:
blacklist = {'None', '0x2A1-2'} blacklist = {'None', '0x2A1-2'}
def seashellCondition(slot_data):
return 'goal' not in slot_data or slot_data['goal'] != 'seashells'
linkedCheckItems = {
'0x2E9': {'item': 'SEASHELL', 'qty': 20, 'condition': seashellCondition},
'0x2A2': {'item': 'TOADSTOOL', 'qty': 1},
'0x2A6-Trade': {'item': 'TRADING_ITEM_YOSHI_DOLL', 'qty': 1},
'0x2B2-Trade': {'item': 'TRADING_ITEM_RIBBON', 'qty': 1},
'0x2FE-Trade': {'item': 'TRADING_ITEM_DOG_FOOD', 'qty': 1},
'0x07B-Trade': {'item': 'TRADING_ITEM_BANANAS', 'qty': 1},
'0x087-Trade': {'item': 'TRADING_ITEM_STICK', 'qty': 1},
'0x2D7-Trade': {'item': 'TRADING_ITEM_HONEYCOMB', 'qty': 1},
'0x019-Trade': {'item': 'TRADING_ITEM_PINEAPPLE', 'qty': 1},
'0x2D9-Trade': {'item': 'TRADING_ITEM_HIBISCUS', 'qty': 1},
'0x2A8-Trade': {'item': 'TRADING_ITEM_LETTER', 'qty': 1},
'0x0CD-Trade': {'item': 'TRADING_ITEM_BROOM', 'qty': 1},
'0x2F5-Trade': {'item': 'TRADING_ITEM_FISHING_HOOK', 'qty': 1},
'0x0C9-Trade': {'item': 'TRADING_ITEM_NECKLACE', 'qty': 1},
'0x297-Trade': {'item': 'TRADING_ITEM_SCALE', 'qty': 1},
}
# in no dungeons boss shuffle, the d3 boss in d7 set 0x20 in fascade's room (0x1BC) # 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) # 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 # entered d3, slime eye flag had already been set (0x15A 0x20). after killing angler fish, bits 0x0C were set
@@ -98,6 +123,8 @@ class LocationTracker:
address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int( address = addressOverrides[check_id] if check_id in addressOverrides else 0xD800 + int(
room, 16) room, 16)
linkedItem = linkedCheckItems[check_id] if check_id in linkedCheckItems else None
if 'Trade' in check_id or 'Owl' in check_id: if 'Trade' in check_id or 'Owl' in check_id:
mask = 0x20 mask = 0x20
@@ -111,13 +138,19 @@ class LocationTracker:
highest_check = max( highest_check = max(
highest_check, alternateAddresses[check_id]) highest_check, alternateAddresses[check_id])
check = Check(check_id, address, mask, check = Check(
alternateAddresses[check_id] if check_id in alternateAddresses else None) check_id,
address,
mask,
(alternateAddresses[check_id] if check_id in alternateAddresses else None),
linkedItem,
)
if check_id == '0x2A3': if check_id == '0x2A3':
self.start_check = check self.start_check = check
self.all_checks.append(check) self.all_checks.append(check)
self.remaining_checks = [check for check in self.all_checks] self.remaining_checks = [check for check in self.all_checks]
self.gameboy.set_cache_limits( self.gameboy.set_checks_range(
lowest_check, highest_check - lowest_check + 1) lowest_check, highest_check - lowest_check + 1)
def has_start_item(self): def has_start_item(self):
@@ -147,10 +180,17 @@ class MagpieBridge:
server = None server = None
checks = None checks = None
item_tracker = None item_tracker = None
gps_tracker: GpsTracker = None
ws = None ws = None
features = [] features = []
slot_data = {} slot_data = {}
def use_entrance_tracker(self):
return "entrances" in self.features \
and self.slot_data \
and "entrance_mapping" in self.slot_data \
and any([k != v for k, v in self.slot_data["entrance_mapping"].items()])
async def handler(self, websocket): async def handler(self, websocket):
self.ws = websocket self.ws = websocket
while True: while True:
@@ -160,13 +200,17 @@ class MagpieBridge:
f"Connected, supported features: {message['features']}") f"Connected, supported features: {message['features']}")
self.features = message["features"] self.features = message["features"]
if message["type"] in ("handshake", "sendFull"): await self.send_handshAck()
if message["type"] == "sendFull":
if "items" in self.features: if "items" in self.features:
await self.send_all_inventory() await self.send_all_inventory()
if "checks" in self.features: if "checks" in self.features:
await self.send_all_checks() await self.send_all_checks()
if "slot_data" in self.features: if "slot_data" in self.features and self.slot_data:
await self.send_slot_data(self.slot_data) await self.send_slot_data(self.slot_data)
if self.use_entrance_tracker():
await self.send_gps(diff=False)
# Translate renamed IDs back to LADXR IDs # Translate renamed IDs back to LADXR IDs
@staticmethod @staticmethod
@@ -177,6 +221,18 @@ class MagpieBridge:
return "0x2A1-1" return "0x2A1-1"
return the_id return the_id
async def send_handshAck(self):
if not self.ws:
return
message = {
"type": "handshAck",
"version": "1.32",
"name": "archipelago-ladx-client",
}
await self.ws.send(json.dumps(message))
async def send_all_checks(self): async def send_all_checks(self):
while self.checks == None: while self.checks == None:
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@@ -185,7 +241,6 @@ class MagpieBridge:
message = { message = {
"type": "check", "type": "check",
"refresh": True, "refresh": True,
"version": "1.0",
"diff": False, "diff": False,
"checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks] "checks": [{"id": self.fixup_id(check.id), "checked": check.value} for check in self.checks]
} }
@@ -200,7 +255,6 @@ class MagpieBridge:
message = { message = {
"type": "check", "type": "check",
"refresh": True, "refresh": True,
"version": "1.0",
"diff": True, "diff": True,
"checks": [{"id": self.fixup_id(check), "checked": True} for check in checks] "checks": [{"id": self.fixup_id(check), "checked": True} for check in checks]
} }
@@ -222,10 +276,17 @@ class MagpieBridge:
return return
await self.item_tracker.sendItems(self.ws, diff=True) await self.item_tracker.sendItems(self.ws, diff=True)
async def send_gps(self, gps): async def send_gps(self, diff: bool=True) -> typing.Dict[str, str]:
if not self.ws: if not self.ws:
return return
await gps.send_location(self.ws)
await self.gps_tracker.send_location(self.ws)
if self.use_entrance_tracker():
if self.slot_data and self.gps_tracker.needs_slot_data:
self.gps_tracker.load_slot_data(self.slot_data)
return await self.gps_tracker.send_entrances(self.ws, diff)
async def send_slot_data(self, slot_data): async def send_slot_data(self, slot_data):
if not self.ws: if not self.ws:

View File

@@ -0,0 +1,291 @@
class EntranceCoord:
name: str
room: int
x: int
y: int
def __init__(self, name: str, room: int, x: int, y: int):
self.name = name
self.room = room
self.x = x
self.y = y
def __repr__(self):
return EntranceCoord.coordString(self.room, self.x, self.y)
def coordString(room: int, x: int, y: int):
return f"{room:#05x}, {x}, {y}"
storage_key = "found_entrances"
room = 0xFFF6
map_id = 0xFFF7
indoor_flag = 0xDBA5
spawn_map = 0xDB60
spawn_room = 0xDB61
spawn_x = 0xDB62
spawn_y = 0xDB63
entrance_room_offset = 0xD800
transition_state = 0xC124
transition_target_x = 0xC12C
transition_target_y = 0xC12D
transition_scroll_x = 0xFF96
transition_scroll_y = 0xFF97
link_motion_state = 0xC11C
transition_sequence = 0xC16B
screen_coord = 0xFFFA
entrance_address_overrides = {
0x312: 0xDDF2,
}
map_map = {
0x00: 0x01,
0x01: 0x01,
0x02: 0x01,
0x03: 0x01,
0x04: 0x01,
0x05: 0x01,
0x06: 0x02,
0x07: 0x02,
0x08: 0x02,
0x09: 0x02,
0x0A: 0x02,
0x0B: 0x02,
0x0C: 0x02,
0x0D: 0x02,
0x0E: 0x02,
0x0F: 0x02,
0x10: 0x02,
0x11: 0x02,
0x12: 0x02,
0x13: 0x02,
0x14: 0x02,
0x15: 0x02,
0x16: 0x02,
0x17: 0x02,
0x18: 0x02,
0x19: 0x02,
0x1D: 0x01,
0x1E: 0x01,
0x1F: 0x01,
0xFF: 0x03,
}
sidescroller_rooms = {
0x2e9: "seashell_mansion:inside",
0x08a: "seashell_mansion",
0x2fd: "mambo:inside",
0x02a: "mambo",
0x1eb: "castle_secret_exit:inside",
0x049: "castle_secret_exit",
0x1ec: "castle_secret_entrance:inside",
0x04a: "castle_secret_entrance",
0x117: "d1:inside", # not a sidescroller, but acts weird
}
entrance_coords = [
EntranceCoord("writes_house:inside", 0x2a8, 80, 124),
EntranceCoord("rooster_grave", 0x92, 88, 82),
EntranceCoord("start_house:inside", 0x2a3, 80, 124),
EntranceCoord("dream_hut", 0x83, 40, 66),
EntranceCoord("papahl_house_right:inside", 0x2a6, 80, 124),
EntranceCoord("papahl_house_right", 0x82, 120, 82),
EntranceCoord("papahl_house_left:inside", 0x2a5, 80, 124),
EntranceCoord("papahl_house_left", 0x82, 88, 82),
EntranceCoord("d2:inside", 0x136, 80, 124),
EntranceCoord("shop", 0x93, 72, 98),
EntranceCoord("armos_maze_cave:inside", 0x2fc, 104, 96),
EntranceCoord("start_house", 0xa2, 88, 82),
EntranceCoord("animal_house3:inside", 0x2d9, 80, 124),
EntranceCoord("trendy_shop", 0xb3, 88, 82),
EntranceCoord("mabe_phone:inside", 0x2cb, 80, 124),
EntranceCoord("mabe_phone", 0xb2, 88, 82),
EntranceCoord("ulrira:inside", 0x2a9, 80, 124),
EntranceCoord("ulrira", 0xb1, 72, 98),
EntranceCoord("moblin_cave:inside", 0x2f0, 80, 124),
EntranceCoord("kennel", 0xa1, 88, 66),
EntranceCoord("madambowwow:inside", 0x2a7, 80, 124),
EntranceCoord("madambowwow", 0xa1, 56, 66),
EntranceCoord("library:inside", 0x1fa, 80, 124),
EntranceCoord("library", 0xb0, 56, 50),
EntranceCoord("d5:inside", 0x1a1, 80, 124),
EntranceCoord("d1", 0xd3, 104, 34),
EntranceCoord("d1:inside", 0x117, 80, 124),
EntranceCoord("d3:inside", 0x152, 80, 124),
EntranceCoord("d3", 0xb5, 104, 32),
EntranceCoord("banana_seller", 0xe3, 72, 48),
EntranceCoord("armos_temple:inside", 0x28f, 80, 124),
EntranceCoord("boomerang_cave", 0xf4, 24, 32),
EntranceCoord("forest_madbatter:inside", 0x1e1, 136, 80),
EntranceCoord("ghost_house", 0xf6, 88, 66),
EntranceCoord("prairie_low_phone:inside", 0x29d, 80, 124),
EntranceCoord("prairie_low_phone", 0xe8, 56, 98),
EntranceCoord("prairie_madbatter_connector_entrance:inside", 0x1f6, 136, 112),
EntranceCoord("prairie_madbatter_connector_entrance", 0xf9, 120, 80),
EntranceCoord("prairie_madbatter_connector_exit", 0xe7, 104, 32),
EntranceCoord("prairie_madbatter_connector_exit:inside", 0x1e5, 40, 48),
EntranceCoord("ghost_house:inside", 0x1e3, 80, 124),
EntranceCoord("prairie_madbatter", 0xe6, 72, 64),
EntranceCoord("d4:inside", 0x17a, 80, 124),
EntranceCoord("d5", 0xd9, 88, 64),
EntranceCoord("prairie_right_cave_bottom:inside", 0x293, 48, 124),
EntranceCoord("prairie_right_cave_bottom", 0xc8, 40, 80),
EntranceCoord("prairie_right_cave_high", 0xb8, 88, 48),
EntranceCoord("prairie_right_cave_high:inside", 0x295, 112, 124),
EntranceCoord("prairie_right_cave_top", 0xb8, 120, 96),
EntranceCoord("prairie_right_cave_top:inside", 0x292, 48, 124),
EntranceCoord("prairie_to_animal_connector:inside", 0x2d0, 40, 64),
EntranceCoord("prairie_to_animal_connector", 0xaa, 136, 64),
EntranceCoord("animal_to_prairie_connector", 0xab, 120, 80),
EntranceCoord("animal_to_prairie_connector:inside", 0x2d1, 120, 64),
EntranceCoord("animal_phone:inside", 0x2e3, 80, 124),
EntranceCoord("animal_phone", 0xdb, 120, 82),
EntranceCoord("animal_house1:inside", 0x2db, 80, 124),
EntranceCoord("animal_house1", 0xcc, 40, 80),
EntranceCoord("animal_house2:inside", 0x2dd, 80, 124),
EntranceCoord("animal_house2", 0xcc, 120, 80),
EntranceCoord("hookshot_cave:inside", 0x2b3, 80, 124),
EntranceCoord("animal_house3", 0xcd, 40, 80),
EntranceCoord("animal_house4:inside", 0x2da, 80, 124),
EntranceCoord("animal_house4", 0xcd, 88, 80),
EntranceCoord("banana_seller:inside", 0x2fe, 80, 124),
EntranceCoord("animal_house5", 0xdd, 88, 66),
EntranceCoord("animal_cave:inside", 0x2f7, 96, 124),
EntranceCoord("animal_cave", 0xcd, 136, 32),
EntranceCoord("d6", 0x8c, 56, 64),
EntranceCoord("madbatter_taltal:inside", 0x1e2, 136, 80),
EntranceCoord("desert_cave", 0xcf, 88, 16),
EntranceCoord("dream_hut:inside", 0x2aa, 80, 124),
EntranceCoord("armos_maze_cave", 0xae, 72, 112),
EntranceCoord("shop:inside", 0x2a1, 80, 124),
EntranceCoord("armos_temple", 0xac, 88, 64),
EntranceCoord("d6_connector_exit:inside", 0x1f0, 56, 16),
EntranceCoord("d6_connector_exit", 0x9c, 88, 16),
EntranceCoord("desert_cave:inside", 0x1f9, 120, 96),
EntranceCoord("d6_connector_entrance:inside", 0x1f1, 136, 96),
EntranceCoord("d6_connector_entrance", 0x9d, 56, 48),
EntranceCoord("armos_fairy:inside", 0x1ac, 80, 124),
EntranceCoord("armos_fairy", 0x8d, 56, 32),
EntranceCoord("raft_return_enter:inside", 0x1f7, 136, 96),
EntranceCoord("raft_return_enter", 0x8f, 8, 32),
EntranceCoord("raft_return_exit", 0x2f, 24, 112),
EntranceCoord("raft_return_exit:inside", 0x1e7, 72, 16),
EntranceCoord("raft_house:inside", 0x2b0, 80, 124),
EntranceCoord("raft_house", 0x3f, 40, 34),
EntranceCoord("heartpiece_swim_cave:inside", 0x1f2, 72, 124),
EntranceCoord("heartpiece_swim_cave", 0x2e, 88, 32),
EntranceCoord("rooster_grave:inside", 0x1f4, 88, 112),
EntranceCoord("d4", 0x2b, 72, 34),
EntranceCoord("castle_phone:inside", 0x2cc, 80, 124),
EntranceCoord("castle_phone", 0x4b, 72, 34),
EntranceCoord("castle_main_entrance:inside", 0x2d3, 80, 124),
EntranceCoord("castle_main_entrance", 0x69, 88, 64),
EntranceCoord("castle_upper_left", 0x59, 24, 48),
EntranceCoord("castle_upper_left:inside", 0x2d5, 80, 124),
EntranceCoord("witch:inside", 0x2a2, 80, 124),
EntranceCoord("castle_upper_right", 0x59, 88, 64),
EntranceCoord("prairie_left_cave2:inside", 0x2f4, 64, 124),
EntranceCoord("castle_jump_cave", 0x78, 40, 112),
EntranceCoord("prairie_left_cave1:inside", 0x2cd, 80, 124),
EntranceCoord("seashell_mansion", 0x8a, 88, 64),
EntranceCoord("prairie_right_phone:inside", 0x29c, 80, 124),
EntranceCoord("prairie_right_phone", 0x88, 88, 82),
EntranceCoord("prairie_left_fairy:inside", 0x1f3, 80, 124),
EntranceCoord("prairie_left_fairy", 0x87, 40, 16),
EntranceCoord("bird_cave:inside", 0x27e, 96, 124),
EntranceCoord("prairie_left_cave2", 0x86, 24, 64),
EntranceCoord("prairie_left_cave1", 0x84, 152, 98),
EntranceCoord("prairie_left_phone:inside", 0x2b4, 80, 124),
EntranceCoord("prairie_left_phone", 0xa4, 56, 66),
EntranceCoord("mamu:inside", 0x2fb, 136, 112),
EntranceCoord("mamu", 0xd4, 136, 48),
EntranceCoord("richard_house:inside", 0x2c7, 80, 124),
EntranceCoord("richard_house", 0xd6, 72, 80),
EntranceCoord("richard_maze:inside", 0x2c9, 128, 124),
EntranceCoord("richard_maze", 0xc6, 56, 80),
EntranceCoord("graveyard_cave_left:inside", 0x2de, 56, 64),
EntranceCoord("graveyard_cave_left", 0x75, 56, 64),
EntranceCoord("graveyard_cave_right:inside", 0x2df, 56, 48),
EntranceCoord("graveyard_cave_right", 0x76, 104, 80),
EntranceCoord("trendy_shop:inside", 0x2a0, 80, 124),
EntranceCoord("d0", 0x77, 120, 46),
EntranceCoord("boomerang_cave:inside", 0x1f5, 72, 124),
EntranceCoord("witch", 0x65, 72, 50),
EntranceCoord("toadstool_entrance:inside", 0x2bd, 80, 124),
EntranceCoord("toadstool_entrance", 0x62, 120, 66),
EntranceCoord("toadstool_exit", 0x50, 136, 50),
EntranceCoord("toadstool_exit:inside", 0x2ab, 80, 124),
EntranceCoord("prairie_madbatter:inside", 0x1e0, 136, 112),
EntranceCoord("hookshot_cave", 0x42, 56, 66),
EntranceCoord("castle_upper_right:inside", 0x2d6, 80, 124),
EntranceCoord("forest_madbatter", 0x52, 104, 48),
EntranceCoord("writes_phone:inside", 0x29b, 80, 124),
EntranceCoord("writes_phone", 0x31, 104, 82),
EntranceCoord("d0:inside", 0x312, 80, 92),
EntranceCoord("writes_house", 0x30, 120, 50),
EntranceCoord("writes_cave_left:inside", 0x2ae, 80, 124),
EntranceCoord("writes_cave_left", 0x20, 136, 50),
EntranceCoord("writes_cave_right:inside", 0x2af, 80, 124),
EntranceCoord("writes_cave_right", 0x21, 24, 50),
EntranceCoord("d6:inside", 0x1d4, 80, 124),
EntranceCoord("d2", 0x24, 56, 34),
EntranceCoord("animal_house5:inside", 0x2d7, 80, 124),
EntranceCoord("moblin_cave", 0x35, 104, 80),
EntranceCoord("crazy_tracy:inside", 0x2ad, 80, 124),
EntranceCoord("crazy_tracy", 0x45, 136, 66),
EntranceCoord("photo_house:inside", 0x2b5, 80, 124),
EntranceCoord("photo_house", 0x37, 72, 66),
EntranceCoord("obstacle_cave_entrance:inside", 0x2b6, 80, 124),
EntranceCoord("obstacle_cave_entrance", 0x17, 56, 50),
EntranceCoord("left_to_right_taltalentrance:inside", 0x2ee, 120, 48),
EntranceCoord("left_to_right_taltalentrance", 0x7, 56, 80),
EntranceCoord("obstacle_cave_outside_chest:inside", 0x2bb, 80, 124),
EntranceCoord("obstacle_cave_outside_chest", 0x18, 104, 18),
EntranceCoord("obstacle_cave_exit:inside", 0x2bc, 48, 124),
EntranceCoord("obstacle_cave_exit", 0x18, 136, 18),
EntranceCoord("papahl_entrance:inside", 0x289, 64, 124),
EntranceCoord("papahl_entrance", 0x19, 136, 64),
EntranceCoord("papahl_exit:inside", 0x28b, 80, 124),
EntranceCoord("papahl_exit", 0xa, 24, 112),
EntranceCoord("rooster_house:inside", 0x29f, 80, 124),
EntranceCoord("rooster_house", 0xa, 72, 34),
EntranceCoord("d7:inside", 0x20e, 80, 124),
EntranceCoord("bird_cave", 0xa, 120, 112),
EntranceCoord("multichest_top:inside", 0x2f2, 80, 124),
EntranceCoord("multichest_top", 0xd, 24, 112),
EntranceCoord("multichest_left:inside", 0x2f9, 32, 124),
EntranceCoord("multichest_left", 0x1d, 24, 48),
EntranceCoord("multichest_right:inside", 0x2fa, 112, 124),
EntranceCoord("multichest_right", 0x1d, 120, 80),
EntranceCoord("right_taltal_connector1:inside", 0x280, 32, 124),
EntranceCoord("right_taltal_connector1", 0x1e, 56, 16),
EntranceCoord("right_taltal_connector3:inside", 0x283, 128, 124),
EntranceCoord("right_taltal_connector3", 0x1e, 120, 16),
EntranceCoord("right_taltal_connector2:inside", 0x282, 112, 124),
EntranceCoord("right_taltal_connector2", 0x1f, 40, 16),
EntranceCoord("right_fairy:inside", 0x1fb, 80, 124),
EntranceCoord("right_fairy", 0x1f, 56, 80),
EntranceCoord("right_taltal_connector4:inside", 0x287, 96, 124),
EntranceCoord("right_taltal_connector4", 0x1f, 88, 64),
EntranceCoord("right_taltal_connector5:inside", 0x28c, 96, 124),
EntranceCoord("right_taltal_connector5", 0x1f, 120, 16),
EntranceCoord("right_taltal_connector6:inside", 0x28e, 112, 124),
EntranceCoord("right_taltal_connector6", 0xf, 72, 80),
EntranceCoord("d7", 0x0e, 88, 48),
EntranceCoord("left_taltal_entrance:inside", 0x2ea, 80, 124),
EntranceCoord("left_taltal_entrance", 0x15, 136, 64),
EntranceCoord("castle_jump_cave:inside", 0x1fd, 88, 80),
EntranceCoord("madbatter_taltal", 0x4, 120, 112),
EntranceCoord("fire_cave_exit:inside", 0x1ee, 24, 64),
EntranceCoord("fire_cave_exit", 0x3, 72, 80),
EntranceCoord("fire_cave_entrance:inside", 0x1fe, 112, 124),
EntranceCoord("fire_cave_entrance", 0x13, 88, 16),
EntranceCoord("phone_d8:inside", 0x299, 80, 124),
EntranceCoord("phone_d8", 0x11, 104, 50),
EntranceCoord("kennel:inside", 0x2b2, 80, 124),
EntranceCoord("d8", 0x10, 88, 16),
EntranceCoord("d8:inside", 0x25d, 80, 124),
]
entrance_lookup = {str(coord): coord for coord in entrance_coords}