711 lines
26 KiB
Python
711 lines
26 KiB
Python
import ModuleUpdate
|
|
ModuleUpdate.update()
|
|
|
|
import Utils
|
|
|
|
if __name__ == "__main__":
|
|
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
|
|
|
|
import asyncio
|
|
import base64
|
|
import binascii
|
|
import colorama
|
|
import io
|
|
import os
|
|
import re
|
|
import select
|
|
import shlex
|
|
import socket
|
|
import struct
|
|
import sys
|
|
import subprocess
|
|
import time
|
|
import typing
|
|
|
|
|
|
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
|
server_loop)
|
|
from NetUtils import ClientStatus
|
|
from worlds.ladx.Common import BASE_ID as LABaseID
|
|
from worlds.ladx.GpsTracker import GpsTracker
|
|
from worlds.ladx.ItemTracker import ItemTracker
|
|
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
|
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
|
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
|
|
|
|
|
class GameboyException(Exception):
|
|
pass
|
|
|
|
|
|
class RetroArchDisconnectError(GameboyException):
|
|
pass
|
|
|
|
|
|
class InvalidEmulatorStateError(GameboyException):
|
|
pass
|
|
|
|
|
|
class BadRetroArchResponse(GameboyException):
|
|
pass
|
|
|
|
|
|
def magpie_logo():
|
|
from kivy.uix.image import CoreImage
|
|
binary_data = """
|
|
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
|
|
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
|
|
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
|
|
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
|
|
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
|
|
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
|
|
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
|
|
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
|
|
binary_data = base64.b64decode(binary_data)
|
|
data = io.BytesIO(binary_data)
|
|
return CoreImage(data, ext="png").texture
|
|
|
|
|
|
class LAClientConstants:
|
|
# Connector version
|
|
VERSION = 0x01
|
|
#
|
|
# Memory locations of LADXR
|
|
ROMGameID = 0x0051 # 4 bytes
|
|
SlotName = 0x0134
|
|
# Unused
|
|
# ROMWorldID = 0x0055
|
|
# ROMConnectorVersion = 0x0056
|
|
# RO: We should only act if this is higher then 6, as it indicates that the game is running normally
|
|
wGameplayType = 0xDB95
|
|
# RO: Starts at 0, increases every time an item is received from the server and processed
|
|
wLinkSyncSequenceNumber = 0xDDF6
|
|
wLinkStatusBits = 0xDDF7 # RW:
|
|
# Bit0: wLinkGive* contains valid data, set from script cleared from ROM.
|
|
wLinkHealth = 0xDB5A
|
|
wLinkGiveItem = 0xDDF8 # RW
|
|
wLinkGiveItemFrom = 0xDDF9 # RW
|
|
# All of these six bytes are unused, we can repurpose
|
|
# wLinkSendItemRoomHigh = 0xDDFA # RO
|
|
# wLinkSendItemRoomLow = 0xDDFB # RO
|
|
# wLinkSendItemTarget = 0xDDFC # RO
|
|
# wLinkSendItemItem = 0xDDFD # RO
|
|
# wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items)
|
|
# RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0
|
|
# wLinkSendShopTarget = 0xDDFF
|
|
|
|
|
|
wRecvIndex = 0xDDFD # Two bytes
|
|
wCheckAddress = 0xC0FF - 0x4
|
|
WRamCheckSize = 0x4
|
|
WRamSafetyValue = bytearray([0]*WRamCheckSize)
|
|
|
|
MinGameplayValue = 0x06
|
|
MaxGameplayValue = 0x1A
|
|
VictoryGameplayAndSub = 0x0102
|
|
|
|
|
|
class RAGameboy():
|
|
cache = []
|
|
cache_start = 0
|
|
cache_size = 0
|
|
last_cache_read = None
|
|
socket = None
|
|
|
|
def __init__(self, address, port) -> None:
|
|
self.address = address
|
|
self.port = port
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
assert (self.socket)
|
|
self.socket.setblocking(False)
|
|
|
|
async def send_command(self, command, timeout=1.0):
|
|
self.send(f'{command}\n')
|
|
response_str = await self.async_recv()
|
|
self.check_command_response(command, response_str)
|
|
return response_str.rstrip()
|
|
|
|
async def get_retroarch_version(self):
|
|
return await self.send_command("VERSION")
|
|
|
|
async def get_retroarch_status(self):
|
|
return await self.send_command("GET_STATUS")
|
|
|
|
def set_cache_limits(self, cache_start, cache_size):
|
|
self.cache_start = cache_start
|
|
self.cache_size = cache_size
|
|
|
|
def send(self, b):
|
|
if type(b) is str:
|
|
b = b.encode('ascii')
|
|
self.socket.sendto(b, (self.address, self.port))
|
|
|
|
def recv(self):
|
|
select.select([self.socket], [], [])
|
|
response, _ = self.socket.recvfrom(4096)
|
|
return response
|
|
|
|
async def async_recv(self, timeout=1.0):
|
|
response = await asyncio.wait_for(asyncio.get_event_loop().sock_recv(self.socket, 4096), timeout)
|
|
return response
|
|
|
|
async def check_safe_gameplay(self, throw=True):
|
|
async def check_wram():
|
|
check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize)
|
|
|
|
if check_values != LAClientConstants.WRamSafetyValue:
|
|
if throw:
|
|
raise InvalidEmulatorStateError()
|
|
return False
|
|
return True
|
|
|
|
if not await check_wram():
|
|
if throw:
|
|
raise InvalidEmulatorStateError()
|
|
return False
|
|
|
|
gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType)
|
|
gameplay_value = gameplay_value[0]
|
|
# In gameplay or credits
|
|
if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1:
|
|
if throw:
|
|
logger.info("invalid emu state")
|
|
raise InvalidEmulatorStateError()
|
|
return False
|
|
if not await check_wram():
|
|
if throw:
|
|
raise InvalidEmulatorStateError()
|
|
return False
|
|
return True
|
|
|
|
# We're sadly unable to update the whole cache at once
|
|
# as RetroArch only gives back some number of bytes at a time
|
|
# So instead read as big as chunks at a time as we can manage
|
|
async def update_cache(self):
|
|
# First read the safety address - if it's invalid, bail
|
|
self.cache = []
|
|
|
|
if not await self.check_safe_gameplay():
|
|
return
|
|
|
|
cache = []
|
|
remaining_size = self.cache_size
|
|
while remaining_size:
|
|
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
|
|
remaining_size -= len(block)
|
|
cache += block
|
|
|
|
if not await self.check_safe_gameplay():
|
|
return
|
|
|
|
self.cache = cache
|
|
self.last_cache_read = time.time()
|
|
|
|
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():
|
|
await self.update_cache()
|
|
if not self.cache:
|
|
return None
|
|
assert (len(self.cache) == self.cache_size)
|
|
for address in addresses:
|
|
assert self.cache_start <= address <= self.cache_start + self.cache_size
|
|
r = {address: self.cache[address - self.cache_start]
|
|
for address in addresses}
|
|
return r
|
|
|
|
async def async_read_memory_safe(self, address, size=1):
|
|
# whenever we do a read for a check, we need to make sure that we aren't reading
|
|
# garbage memory values - we also need to protect against reading a value, then the emulator resetting
|
|
#
|
|
# ...actually, we probably _only_ need the post check
|
|
|
|
# Check before read
|
|
if not await self.check_safe_gameplay():
|
|
return None
|
|
|
|
# Do read
|
|
r = await self.async_read_memory(address, size)
|
|
|
|
# Check after read
|
|
if not await self.check_safe_gameplay():
|
|
return None
|
|
|
|
return r
|
|
|
|
def check_command_response(self, command: str, response: bytes):
|
|
if command == "VERSION":
|
|
ok = re.match(r"\d+\.\d+\.\d+", response.decode('ascii')) is not None
|
|
else:
|
|
ok = response.startswith(command.encode())
|
|
if not ok:
|
|
logger.warning(f"Bad response to command {command} - {response}")
|
|
raise BadRetroArchResponse()
|
|
|
|
def read_memory(self, address, size=1):
|
|
command = "READ_CORE_MEMORY"
|
|
|
|
self.send(f'{command} {hex(address)} {size}\n')
|
|
response = self.recv()
|
|
|
|
self.check_command_response(command, response)
|
|
|
|
splits = response.decode().split(" ", 2)
|
|
# Ignore the address for now
|
|
if splits[2][:2] == "-1":
|
|
raise BadRetroArchResponse()
|
|
|
|
# TODO: check response address, check hex behavior between RA and BH
|
|
|
|
return bytearray.fromhex(splits[2])
|
|
|
|
async def async_read_memory(self, address, size=1):
|
|
command = "READ_CORE_MEMORY"
|
|
|
|
self.send(f'{command} {hex(address)} {size}\n')
|
|
response = await self.async_recv()
|
|
self.check_command_response(command, response)
|
|
response = response[:-1]
|
|
splits = response.decode().split(" ", 2)
|
|
try:
|
|
response_addr = int(splits[1], 16)
|
|
except ValueError:
|
|
raise BadRetroArchResponse()
|
|
|
|
if response_addr != address:
|
|
raise BadRetroArchResponse()
|
|
|
|
ret = bytearray.fromhex(splits[2])
|
|
if len(ret) > size:
|
|
raise BadRetroArchResponse()
|
|
return ret
|
|
|
|
def write_memory(self, address, bytes):
|
|
command = "WRITE_CORE_MEMORY"
|
|
|
|
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
|
|
select.select([self.socket], [], [])
|
|
response, _ = self.socket.recvfrom(4096)
|
|
self.check_command_response(command, response)
|
|
splits = response.decode().split(" ", 3)
|
|
|
|
assert (splits[0] == command)
|
|
|
|
if splits[2] == "-1":
|
|
logger.info(splits[3])
|
|
|
|
|
|
class LinksAwakeningClient():
|
|
socket = None
|
|
gameboy = None
|
|
tracker = None
|
|
auth = None
|
|
game_crc = None
|
|
pending_deathlink = False
|
|
deathlink_debounce = True
|
|
recvd_checks = {}
|
|
retroarch_address = None
|
|
retroarch_port = None
|
|
gameboy = None
|
|
|
|
def msg(self, m):
|
|
logger.info(m)
|
|
s = f"SHOW_MSG {m}\n"
|
|
self.gameboy.send(s)
|
|
|
|
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
|
|
self.retroarch_address = retroarch_address
|
|
self.retroarch_port = retroarch_port
|
|
pass
|
|
|
|
stop_bizhawk_spam = False
|
|
async def wait_for_retroarch_connection(self):
|
|
if not self.stop_bizhawk_spam:
|
|
logger.info("Waiting on connection to Retroarch...")
|
|
self.stop_bizhawk_spam = True
|
|
self.gameboy = RAGameboy(self.retroarch_address, self.retroarch_port)
|
|
|
|
while True:
|
|
try:
|
|
version = await self.gameboy.get_retroarch_version()
|
|
NO_CONTENT = b"GET_STATUS CONTENTLESS"
|
|
status = NO_CONTENT
|
|
core_type = None
|
|
GAME_BOY = b"game_boy"
|
|
while status == NO_CONTENT or core_type != GAME_BOY:
|
|
status = await self.gameboy.get_retroarch_status()
|
|
if status.count(b" ") < 2:
|
|
await asyncio.sleep(1.0)
|
|
continue
|
|
GET_STATUS, PLAYING, info = status.split(b" ", 2)
|
|
if status.count(b",") < 2:
|
|
await asyncio.sleep(1.0)
|
|
continue
|
|
core_type, rom_name, self.game_crc = info.split(b",", 2)
|
|
if core_type != GAME_BOY:
|
|
logger.info(
|
|
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
|
|
await asyncio.sleep(1.0)
|
|
continue
|
|
self.stop_bizhawk_spam = False
|
|
logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} "
|
|
f"running {rom_name.decode('ascii', errors='replace')}")
|
|
return
|
|
except (BlockingIOError, TimeoutError, ConnectionResetError):
|
|
await asyncio.sleep(1.0)
|
|
pass
|
|
|
|
async def reset_auth(self):
|
|
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
|
|
self.auth = auth
|
|
|
|
async def wait_and_init_tracker(self):
|
|
await self.wait_for_game_ready()
|
|
self.tracker = LocationTracker(self.gameboy)
|
|
self.item_tracker = ItemTracker(self.gameboy)
|
|
self.gps_tracker = GpsTracker(self.gameboy)
|
|
|
|
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
|
|
if not self.tracker.has_start_item():
|
|
return
|
|
|
|
# Spin until we either:
|
|
# get an exception from a bad read (emu shut down or reset)
|
|
# beat the game
|
|
# the client handles the last pending item
|
|
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
|
while not (await self.is_victory()) and status & 1 == 1:
|
|
time.sleep(0.1)
|
|
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
|
|
|
|
item_id -= LABaseID
|
|
# The player name table only goes up to 100, so don't go past that
|
|
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
|
|
if from_player > 100:
|
|
from_player = 100
|
|
|
|
next_index += 1
|
|
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
|
|
item_id, from_player])
|
|
status |= 1
|
|
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
|
|
self.gameboy.write_memory(LAClientConstants.wRecvIndex, struct.pack(">H", next_index))
|
|
|
|
should_reset_auth = False
|
|
async def wait_for_game_ready(self):
|
|
logger.info("Waiting on game to be in valid state...")
|
|
while not await self.gameboy.check_safe_gameplay(throw=False):
|
|
if self.should_reset_auth:
|
|
self.should_reset_auth = False
|
|
raise GameboyException("Resetting due to wrong archipelago server")
|
|
logger.info("Game connection ready!")
|
|
|
|
async def is_victory(self):
|
|
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
|
|
|
|
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
|
|
await self.tracker.readChecks(item_get_cb)
|
|
await self.item_tracker.readItems()
|
|
await self.gps_tracker.read_location()
|
|
|
|
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
|
|
if self.deathlink_debounce and current_health != 0:
|
|
self.deathlink_debounce = False
|
|
elif not self.deathlink_debounce and current_health == 0:
|
|
# logger.info("YOU DIED.")
|
|
await deathlink_cb()
|
|
self.deathlink_debounce = True
|
|
|
|
if self.pending_deathlink:
|
|
logger.info("Got a deathlink")
|
|
self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0])
|
|
self.pending_deathlink = False
|
|
self.deathlink_debounce = True
|
|
|
|
if await self.is_victory():
|
|
await win_cb()
|
|
|
|
recv_index = struct.unpack(">H", await self.gameboy.async_read_memory(LAClientConstants.wRecvIndex, 2))[0]
|
|
|
|
# Play back one at a time
|
|
if recv_index in self.recvd_checks:
|
|
item = self.recvd_checks[recv_index]
|
|
await self.recved_item_from_ap(item.item, item.player, recv_index)
|
|
|
|
|
|
all_tasks = set()
|
|
|
|
def create_task_log_exception(awaitable) -> asyncio.Task:
|
|
async def _log_exception(awaitable):
|
|
try:
|
|
return await awaitable
|
|
except Exception as e:
|
|
logger.exception(e)
|
|
pass
|
|
finally:
|
|
all_tasks.remove(task)
|
|
task = asyncio.create_task(_log_exception(awaitable))
|
|
all_tasks.add(task)
|
|
|
|
|
|
class LinksAwakeningContext(CommonContext):
|
|
tags = {"AP"}
|
|
game = "Links Awakening DX"
|
|
items_handling = 0b101
|
|
want_slot_data = True
|
|
la_task = None
|
|
client = None
|
|
# TODO: does this need to re-read on reset?
|
|
found_checks = []
|
|
last_resend = time.time()
|
|
|
|
magpie_enabled = False
|
|
magpie = None
|
|
magpie_task = None
|
|
won = False
|
|
|
|
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
|
self.client = LinksAwakeningClient()
|
|
self.slot_data = {}
|
|
|
|
if magpie:
|
|
self.magpie_enabled = True
|
|
self.magpie = MagpieBridge()
|
|
super().__init__(server_address, password)
|
|
|
|
def run_gui(self) -> None:
|
|
import webbrowser
|
|
import kvui
|
|
from kvui import Button, GameManager
|
|
from kivy.uix.image import Image
|
|
|
|
class LADXManager(GameManager):
|
|
logging_pairs = [
|
|
("Client", "Archipelago"),
|
|
("Tracker", "Tracker"),
|
|
]
|
|
base_title = "Archipelago Links Awakening DX Client"
|
|
|
|
def build(self):
|
|
b = super().build()
|
|
|
|
if self.ctx.magpie_enabled:
|
|
button = Button(text="", size=(30, 30), size_hint_x=None,
|
|
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
|
image = Image(size=(16, 16), texture=magpie_logo())
|
|
button.add_widget(image)
|
|
|
|
def set_center(_, center):
|
|
image.center = center
|
|
button.bind(center=set_center)
|
|
|
|
self.connect_layout.add_widget(button)
|
|
return b
|
|
|
|
self.ui = LADXManager(self)
|
|
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
|
|
|
async def send_checks(self):
|
|
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
|
|
await self.send_msgs(message)
|
|
|
|
had_invalid_slot_data = None
|
|
def event_invalid_slot(self):
|
|
# The next time we try to connect, reset the game loop for new auth
|
|
self.had_invalid_slot_data = True
|
|
self.auth = None
|
|
# Don't try to autoreconnect, it will just fail
|
|
self.disconnected_intentionally = True
|
|
CommonContext.event_invalid_slot(self)
|
|
|
|
ENABLE_DEATHLINK = False
|
|
async def send_deathlink(self):
|
|
if self.ENABLE_DEATHLINK:
|
|
message = [{"cmd": 'Deathlink',
|
|
'time': time.time(),
|
|
'cause': 'Had a nightmare',
|
|
# 'source': self.slot_info[self.slot].name,
|
|
}]
|
|
await self.send_msgs(message)
|
|
|
|
async def send_victory(self):
|
|
if not self.won:
|
|
message = [{"cmd": "StatusUpdate",
|
|
"status": ClientStatus.CLIENT_GOAL}]
|
|
logger.info("victory!")
|
|
await self.send_msgs(message)
|
|
self.won = True
|
|
|
|
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
|
if self.ENABLE_DEATHLINK:
|
|
self.client.pending_deathlink = True
|
|
|
|
def new_checks(self, item_ids, ladxr_ids):
|
|
self.found_checks += item_ids
|
|
create_task_log_exception(self.send_checks())
|
|
if self.magpie_enabled:
|
|
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
|
|
|
async def server_auth(self, password_requested: bool = False):
|
|
if password_requested and not self.password:
|
|
await super(LinksAwakeningContext, self).server_auth(password_requested)
|
|
|
|
if self.had_invalid_slot_data:
|
|
# We are connecting when previously we had the wrong ROM or server - just in case
|
|
# re-read the ROM so that if the user had the correct address but wrong ROM, we
|
|
# allow a successful reconnect
|
|
self.client.should_reset_auth = True
|
|
self.had_invalid_slot_data = False
|
|
|
|
while self.client.auth == None:
|
|
await asyncio.sleep(0.1)
|
|
|
|
# Just return if we're closing
|
|
if self.exit_event.is_set():
|
|
return
|
|
self.auth = self.client.auth
|
|
await self.send_connect()
|
|
|
|
def on_package(self, cmd: str, args: dict):
|
|
if cmd == "Connected":
|
|
self.game = self.slot_info[self.slot].game
|
|
self.slot_data = args.get("slot_data", {})
|
|
|
|
# TODO - use watcher_event
|
|
if cmd == "ReceivedItems":
|
|
for index, item in enumerate(args["items"], start=args["index"]):
|
|
self.client.recvd_checks[index] = item
|
|
|
|
async def sync(self):
|
|
sync_msg = [{'cmd': 'Sync'}]
|
|
await self.send_msgs(sync_msg)
|
|
|
|
item_id_lookup = get_locations_to_id()
|
|
|
|
async def run_game_loop(self):
|
|
def on_item_get(ladxr_checks):
|
|
checks = [self.item_id_lookup[meta_to_name(
|
|
checkMetadataTable[check.id])] for check in ladxr_checks]
|
|
self.new_checks(checks, [check.id for check in ladxr_checks])
|
|
|
|
async def victory():
|
|
await self.send_victory()
|
|
|
|
async def deathlink():
|
|
await self.send_deathlink()
|
|
|
|
if self.magpie_enabled:
|
|
self.magpie_task = asyncio.create_task(self.magpie.serve())
|
|
|
|
# yield to allow UI to start
|
|
await asyncio.sleep(0)
|
|
|
|
while True:
|
|
try:
|
|
# TODO: cancel all client tasks
|
|
if not self.client.stop_bizhawk_spam:
|
|
logger.info("(Re)Starting game loop")
|
|
self.found_checks.clear()
|
|
# On restart of game loop, clear all checks, just in case we swapped ROMs
|
|
# this isn't totally neccessary, but is extra safety against cross-ROM contamination
|
|
self.client.recvd_checks.clear()
|
|
await self.client.wait_for_retroarch_connection()
|
|
await self.client.reset_auth()
|
|
# If we find ourselves with new auth after the reset, reconnect
|
|
if self.auth and self.client.auth != self.auth:
|
|
# It would be neat to reconnect here, but connection needs this loop to be running
|
|
logger.info("Detected new ROM, disconnecting...")
|
|
await self.disconnect()
|
|
continue
|
|
|
|
if not self.client.recvd_checks:
|
|
await self.sync()
|
|
|
|
await self.client.wait_and_init_tracker()
|
|
|
|
while True:
|
|
await self.client.main_tick(on_item_get, victory, deathlink)
|
|
await asyncio.sleep(0.1)
|
|
now = time.time()
|
|
if self.last_resend + 5.0 < now:
|
|
self.last_resend = now
|
|
await self.send_checks()
|
|
if self.magpie_enabled:
|
|
try:
|
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
|
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
|
|
except Exception:
|
|
# Don't let magpie errors take out the client
|
|
pass
|
|
if self.client.should_reset_auth:
|
|
self.client.should_reset_auth = False
|
|
raise GameboyException("Resetting due to wrong archipelago server")
|
|
except (GameboyException, asyncio.TimeoutError, TimeoutError, ConnectionResetError):
|
|
await asyncio.sleep(1.0)
|
|
|
|
def run_game(romfile: str) -> None:
|
|
auto_start = typing.cast(typing.Union[bool, str],
|
|
Utils.get_options()["ladx_options"].get("rom_start", True))
|
|
if auto_start is True:
|
|
import webbrowser
|
|
webbrowser.open(romfile)
|
|
elif isinstance(auto_start, str):
|
|
args = shlex.split(auto_start)
|
|
# Specify full path to ROM as we are going to cd in popen
|
|
full_rom_path = os.path.realpath(romfile)
|
|
args.append(full_rom_path)
|
|
try:
|
|
# set cwd so that paths to lua scripts are always relative to our client
|
|
if getattr(sys, 'frozen', False):
|
|
# The application is frozen
|
|
script_dir = os.path.dirname(sys.executable)
|
|
else:
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=script_dir)
|
|
except FileNotFoundError:
|
|
logger.error(f"Couldn't launch ROM, {args[0]} is missing")
|
|
|
|
async def main():
|
|
parser = get_base_parser(description="Link's Awakening Client.")
|
|
parser.add_argument("--url", help="Archipelago connection url")
|
|
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
|
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
|
help='Path to a .apladx Archipelago Binary Patch file')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.diff_file:
|
|
import Patch
|
|
logger.info("patch file was supplied - creating rom...")
|
|
meta, rom_file = Patch.create_rom_file(args.diff_file)
|
|
if "server" in meta and not args.connect:
|
|
args.connect = meta["server"]
|
|
logger.info(f"wrote rom file to {rom_file}")
|
|
|
|
|
|
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
|
|
|
|
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
|
|
|
# TODO: nothing about the lambda about has to be in a lambda
|
|
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
|
|
if gui_enabled:
|
|
ctx.run_gui()
|
|
ctx.run_cli()
|
|
|
|
# Down below run_gui so that we get errors out of the process
|
|
if args.diff_file:
|
|
run_game(rom_file)
|
|
|
|
await ctx.exit_event.wait()
|
|
await ctx.shutdown()
|
|
|
|
if __name__ == '__main__':
|
|
colorama.init()
|
|
asyncio.run(main())
|
|
colorama.deinit()
|