mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
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:
@@ -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
|
||||||
@@ -536,6 +601,12 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
logger.info("victory!")
|
logger.info("victory!")
|
||||||
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:
|
||||||
@@ -576,6 +647,12 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
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'}]
|
||||||
@@ -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
|
||||||
|
@@ -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)
|
||||||
last_message = {}
|
self.screen_x = coords & 0x0F
|
||||||
async def send_location(self, socket, diff=False):
|
self.screen_y = (coords & 0xF0) >> 4
|
||||||
if self.room is None:
|
|
||||||
|
async def read_entrances(self):
|
||||||
|
if not self.last_different_room or not self.entrance_mapping:
|
||||||
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)
|
||||||
|
@@ -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": [],
|
||||||
}
|
}
|
||||||
|
@@ -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:
|
||||||
@@ -159,14 +199,18 @@ class MagpieBridge:
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"Connected, supported features: {message['features']}")
|
f"Connected, supported features: {message['features']}")
|
||||||
self.features = message["features"]
|
self.features = message["features"]
|
||||||
|
|
||||||
|
await self.send_handshAck()
|
||||||
|
|
||||||
if message["type"] in ("handshake", "sendFull"):
|
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
|
||||||
@@ -176,6 +220,18 @@ class MagpieBridge:
|
|||||||
if the_id == "0x2A7":
|
if the_id == "0x2A7":
|
||||||
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:
|
||||||
@@ -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:
|
||||||
|
291
worlds/ladx/TrackerConsts.py
Normal file
291
worlds/ladx/TrackerConsts.py
Normal 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}
|
Reference in New Issue
Block a user