mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
BizHawkClient: Add BizHawkClient (#1978)
Adds a generic client that can communicate with BizHawk. Similar to SNIClient, but for arbitrary systems and doesn't have an intermediary application like SNI.
This commit is contained in:
326
worlds/_bizhawk/__init__.py
Normal file
326
worlds/_bizhawk/__init__.py
Normal file
@@ -0,0 +1,326 @@
|
||||
"""
|
||||
A module for interacting with BizHawk through `connector_bizhawk_generic.lua`.
|
||||
|
||||
Any mention of `domain` in this module refers to the names BizHawk gives to memory domains in its own lua api. They are
|
||||
naively passed to BizHawk without validation or modification.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import enum
|
||||
import json
|
||||
import typing
|
||||
|
||||
|
||||
BIZHAWK_SOCKET_PORT = 43055
|
||||
EXPECTED_SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class ConnectionStatus(enum.IntEnum):
|
||||
NOT_CONNECTED = 1
|
||||
TENTATIVE = 2
|
||||
CONNECTED = 3
|
||||
|
||||
|
||||
class BizHawkContext:
|
||||
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
|
||||
connection_status: ConnectionStatus
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.streams = None
|
||||
self.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
|
||||
class NotConnectedError(Exception):
|
||||
"""Raised when something tries to make a request to the connector script before a connection has been established"""
|
||||
pass
|
||||
|
||||
|
||||
class RequestFailedError(Exception):
|
||||
"""Raised when the connector script did not respond to a request"""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectorError(Exception):
|
||||
"""Raised when the connector script encounters an error while processing a request"""
|
||||
pass
|
||||
|
||||
|
||||
class SyncError(Exception):
|
||||
"""Raised when the connector script responded with a mismatched response type"""
|
||||
pass
|
||||
|
||||
|
||||
async def connect(ctx: BizHawkContext) -> bool:
|
||||
"""Attempts to establish a connection with the connector script. Returns True if successful."""
|
||||
try:
|
||||
ctx.streams = await asyncio.open_connection("localhost", BIZHAWK_SOCKET_PORT)
|
||||
ctx.connection_status = ConnectionStatus.TENTATIVE
|
||||
return True
|
||||
except (TimeoutError, ConnectionRefusedError):
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
return False
|
||||
|
||||
|
||||
def disconnect(ctx: BizHawkContext) -> None:
|
||||
"""Closes the connection to the connector script."""
|
||||
if ctx.streams is not None:
|
||||
ctx.streams[1].close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
|
||||
|
||||
async def get_script_version(ctx: BizHawkContext) -> int:
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write("VERSION".encode("ascii") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
version = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if version == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
return int(version.decode("ascii"))
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
|
||||
|
||||
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
|
||||
"""Sends a list of requests to the BizHawk connector and returns their responses.
|
||||
|
||||
It's likely you want to use the wrapper functions instead of this."""
|
||||
if ctx.streams is None:
|
||||
raise NotConnectedError("You tried to send a request before a connection to BizHawk was made")
|
||||
|
||||
try:
|
||||
reader, writer = ctx.streams
|
||||
writer.write(json.dumps(req_list).encode("utf-8") + b"\n")
|
||||
await asyncio.wait_for(writer.drain(), timeout=5)
|
||||
|
||||
res = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
|
||||
if res == b"":
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection closed")
|
||||
|
||||
if ctx.connection_status == ConnectionStatus.TENTATIVE:
|
||||
ctx.connection_status = ConnectionStatus.CONNECTED
|
||||
|
||||
ret = json.loads(res.decode("utf-8"))
|
||||
for response in ret:
|
||||
if response["type"] == "ERROR":
|
||||
raise ConnectorError(response["err"])
|
||||
|
||||
return ret
|
||||
except asyncio.TimeoutError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection timed out") from exc
|
||||
except ConnectionResetError as exc:
|
||||
writer.close()
|
||||
ctx.streams = None
|
||||
ctx.connection_status = ConnectionStatus.NOT_CONNECTED
|
||||
raise RequestFailedError("Connection reset") from exc
|
||||
|
||||
|
||||
async def ping(ctx: BizHawkContext) -> None:
|
||||
"""Sends a PING request and receives a PONG response."""
|
||||
res = (await send_requests(ctx, [{"type": "PING"}]))[0]
|
||||
|
||||
if res["type"] != "PONG":
|
||||
raise SyncError(f"Expected response of type PONG but got {res['type']}")
|
||||
|
||||
|
||||
async def get_hash(ctx: BizHawkContext) -> str:
|
||||
"""Gets the system name for the currently loaded ROM"""
|
||||
res = (await send_requests(ctx, [{"type": "HASH"}]))[0]
|
||||
|
||||
if res["type"] != "HASH_RESPONSE":
|
||||
raise SyncError(f"Expected response of type HASH_RESPONSE but got {res['type']}")
|
||||
|
||||
return res["value"]
|
||||
|
||||
|
||||
async def get_system(ctx: BizHawkContext) -> str:
|
||||
"""Gets the system name for the currently loaded ROM"""
|
||||
res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0]
|
||||
|
||||
if res["type"] != "SYSTEM_RESPONSE":
|
||||
raise SyncError(f"Expected response of type SYSTEM_RESPONSE but got {res['type']}")
|
||||
|
||||
return res["value"]
|
||||
|
||||
|
||||
async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]:
|
||||
"""Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have
|
||||
entries."""
|
||||
res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0]
|
||||
|
||||
if res["type"] != "PREFERRED_CORES_RESPONSE":
|
||||
raise SyncError(f"Expected response of type PREFERRED_CORES_RESPONSE but got {res['type']}")
|
||||
|
||||
return res["value"]
|
||||
|
||||
|
||||
async def lock(ctx: BizHawkContext) -> None:
|
||||
"""Locks BizHawk in anticipation of receiving more requests this frame.
|
||||
|
||||
Consider using guarded reads and writes instead of locks if possible.
|
||||
|
||||
While locked, emulation will halt and the connector will block on incoming requests until an `UNLOCK` request is
|
||||
sent. Remember to unlock when you're done, or the emulator will appear to freeze.
|
||||
|
||||
Sending multiple lock commands is the same as sending one."""
|
||||
res = (await send_requests(ctx, [{"type": "LOCK"}]))[0]
|
||||
|
||||
if res["type"] != "LOCKED":
|
||||
raise SyncError(f"Expected response of type LOCKED but got {res['type']}")
|
||||
|
||||
|
||||
async def unlock(ctx: BizHawkContext) -> None:
|
||||
"""Unlocks BizHawk to allow it to resume emulation. See `lock` for more info.
|
||||
|
||||
Sending multiple unlock commands is the same as sending one."""
|
||||
res = (await send_requests(ctx, [{"type": "UNLOCK"}]))[0]
|
||||
|
||||
if res["type"] != "UNLOCKED":
|
||||
raise SyncError(f"Expected response of type UNLOCKED but got {res['type']}")
|
||||
|
||||
|
||||
async def display_message(ctx: BizHawkContext, message: str) -> None:
|
||||
"""Displays the provided message in BizHawk's message queue."""
|
||||
res = (await send_requests(ctx, [{"type": "DISPLAY_MESSAGE", "message": message}]))[0]
|
||||
|
||||
if res["type"] != "DISPLAY_MESSAGE_RESPONSE":
|
||||
raise SyncError(f"Expected response of type DISPLAY_MESSAGE_RESPONSE but got {res['type']}")
|
||||
|
||||
|
||||
async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
|
||||
"""Sets the minimum amount of time in seconds to wait between queued messages. The default value of 0 will allow one
|
||||
new message to display per frame."""
|
||||
res = (await send_requests(ctx, [{"type": "SET_MESSAGE_INTERVAL", "value": value}]))[0]
|
||||
|
||||
if res["type"] != "SET_MESSAGE_INTERVAL_RESPONSE":
|
||||
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
|
||||
|
||||
|
||||
async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]],
|
||||
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]:
|
||||
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
|
||||
value.
|
||||
|
||||
Items in read_list should be organized (address, size, domain) where
|
||||
- `address` is the address of the first byte of data
|
||||
- `size` is the number of bytes to read
|
||||
- `domain` is the name of the region of memory the address corresponds to
|
||||
|
||||
Items in `guard_list` should be organized `(address, expected_data, domain)` where
|
||||
- `address` is the address of the first byte of data
|
||||
- `expected_data` is the bytes that the data starting at this address is expected to match
|
||||
- `domain` is the name of the region of memory the address corresponds to
|
||||
|
||||
Returns None if any item in guard_list failed to validate. Otherwise returns a list of bytes in the order they
|
||||
were requested."""
|
||||
res = await send_requests(ctx, [{
|
||||
"type": "GUARD",
|
||||
"address": address,
|
||||
"expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"),
|
||||
"domain": domain
|
||||
} for address, expected_data, domain in guard_list] + [{
|
||||
"type": "READ",
|
||||
"address": address,
|
||||
"size": size,
|
||||
"domain": domain
|
||||
} for address, size, domain in read_list])
|
||||
|
||||
ret: typing.List[bytes] = []
|
||||
for item in res:
|
||||
if item["type"] == "GUARD_RESPONSE":
|
||||
if not item["value"]:
|
||||
return None
|
||||
else:
|
||||
if item["type"] != "READ_RESPONSE":
|
||||
raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {res['type']}")
|
||||
|
||||
ret.append(base64.b64decode(item["value"]))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
|
||||
"""Reads data at 1 or more addresses.
|
||||
|
||||
Items in `read_list` should be organized `(address, size, domain)` where
|
||||
- `address` is the address of the first byte of data
|
||||
- `size` is the number of bytes to read
|
||||
- `domain` is the name of the region of memory the address corresponds to
|
||||
|
||||
Returns a list of bytes in the order they were requested."""
|
||||
return await guarded_read(ctx, read_list, [])
|
||||
|
||||
|
||||
async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]],
|
||||
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool:
|
||||
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
|
||||
|
||||
Items in `write_list` should be organized `(address, value, domain)` where
|
||||
- `address` is the address of the first byte of data
|
||||
- `value` is a list of bytes to write, in order, starting at `address`
|
||||
- `domain` is the name of the region of memory the address corresponds to
|
||||
|
||||
Items in `guard_list` should be organized `(address, expected_data, domain)` where
|
||||
- `address` is the address of the first byte of data
|
||||
- `expected_data` is the bytes that the data starting at this address is expected to match
|
||||
- `domain` is the name of the region of memory the address corresponds to
|
||||
|
||||
Returns False if any item in guard_list failed to validate. Otherwise returns True."""
|
||||
res = await send_requests(ctx, [{
|
||||
"type": "GUARD",
|
||||
"address": address,
|
||||
"expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"),
|
||||
"domain": domain
|
||||
} for address, expected_data, domain in guard_list] + [{
|
||||
"type": "WRITE",
|
||||
"address": address,
|
||||
"value": base64.b64encode(bytes(value)).decode("ascii"),
|
||||
"domain": domain
|
||||
} for address, value, domain in write_list])
|
||||
|
||||
for item in res:
|
||||
if item["type"] == "GUARD_RESPONSE":
|
||||
if not item["value"]:
|
||||
return False
|
||||
else:
|
||||
if item["type"] != "WRITE_RESPONSE":
|
||||
raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {res['type']}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None:
|
||||
"""Writes data to 1 or more addresses.
|
||||
|
||||
Items in write_list should be organized `(address, value, domain)` where
|
||||
- `address` is the address of the first byte of data
|
||||
- `value` is a list of bytes to write, in order, starting at `address`
|
||||
- `domain` is the name of the region of memory the address corresponds to"""
|
||||
await guarded_write(ctx, write_list, [])
|
87
worlds/_bizhawk/client.py
Normal file
87
worlds/_bizhawk/client.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
A module containing the BizHawkClient base class and metaclass
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .context import BizHawkClientContext
|
||||
else:
|
||||
BizHawkClientContext = object
|
||||
|
||||
|
||||
class AutoBizHawkClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
|
||||
new_class = super().__new__(cls, name, bases, namespace)
|
||||
|
||||
if "system" in namespace:
|
||||
systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"]))
|
||||
if systems not in AutoBizHawkClientRegister.game_handlers:
|
||||
AutoBizHawkClientRegister.game_handlers[systems] = {}
|
||||
|
||||
if "game" in namespace:
|
||||
AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class()
|
||||
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHawkClient]:
|
||||
for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
|
||||
if system in systems:
|
||||
for handler in handlers.values():
|
||||
if await handler.validate_rom(ctx):
|
||||
return handler
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
|
||||
system: ClassVar[Union[str, Tuple[str, ...]]]
|
||||
"""The system that the game this client is for runs on"""
|
||||
|
||||
game: ClassVar[str]
|
||||
"""The game this client is for"""
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: BizHawkClientContext) -> bool:
|
||||
"""Should return whether the currently loaded ROM should be handled by this client. You might read the game name
|
||||
from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the
|
||||
client class, so you do not need to check the system yourself.
|
||||
|
||||
Once this function has determined that the ROM should be handled by this client, it should also modify `ctx`
|
||||
as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...)."""
|
||||
...
|
||||
|
||||
async def set_auth(self, ctx: BizHawkClientContext) -> None:
|
||||
"""Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot
|
||||
name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their
|
||||
username."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def game_watcher(self, ctx: BizHawkClientContext) -> None:
|
||||
"""Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed
|
||||
to have passed your validator when this function is called, and the emulator is very likely to be connected."""
|
||||
...
|
||||
|
||||
def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None:
|
||||
"""For handling packages from the server. Called from `BizHawkClientContext.on_package`."""
|
||||
pass
|
||||
|
||||
|
||||
def launch_client(*args) -> None:
|
||||
from .context import launch
|
||||
launch_subprocess(launch, name="BizHawkClient")
|
||||
|
||||
|
||||
if not any(component.script_name == "BizHawkClient" for component in components):
|
||||
components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
|
||||
file_identifier=SuffixIdentifier()))
|
188
worlds/_bizhawk/context.py
Normal file
188
worlds/_bizhawk/context.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
A module containing context and functions relevant to running the client. This module should only be imported for type
|
||||
checking or launching the client, otherwise it will probably cause circular import issues.
|
||||
"""
|
||||
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled
|
||||
import Patch
|
||||
import Utils
|
||||
|
||||
from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \
|
||||
get_system, ping
|
||||
from .client import BizHawkClient, AutoBizHawkClientRegister
|
||||
|
||||
|
||||
EXPECTED_SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class BizHawkClientCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_bh(self):
|
||||
"""Shows the current status of the client's connection to BizHawk"""
|
||||
if isinstance(self.ctx, BizHawkClientContext):
|
||||
if self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED:
|
||||
logger.info("BizHawk Connection Status: Not Connected")
|
||||
elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.TENTATIVE:
|
||||
logger.info("BizHawk Connection Status: Tentatively Connected")
|
||||
elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.CONNECTED:
|
||||
logger.info("BizHawk Connection Status: Connected")
|
||||
|
||||
|
||||
class BizHawkClientContext(CommonContext):
|
||||
command_processor = BizHawkClientCommandProcessor
|
||||
client_handler: Optional[BizHawkClient]
|
||||
slot_data: Optional[Dict[str, Any]] = None
|
||||
rom_hash: Optional[str] = None
|
||||
bizhawk_ctx: BizHawkContext
|
||||
|
||||
watcher_timeout: float
|
||||
"""The maximum amount of time the game watcher loop will wait for an update from the server before executing"""
|
||||
|
||||
def __init__(self, server_address: Optional[str], password: Optional[str]):
|
||||
super().__init__(server_address, password)
|
||||
self.client_handler = None
|
||||
self.bizhawk_ctx = BizHawkContext()
|
||||
self.watcher_timeout = 0.5
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class BizHawkManager(GameManager):
|
||||
base_title = "Archipelago BizHawk Client"
|
||||
|
||||
self.ui = BizHawkManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
def on_package(self, cmd, args):
|
||||
if cmd == "Connected":
|
||||
self.slot_data = args.get("slot_data", None)
|
||||
|
||||
if self.client_handler is not None:
|
||||
self.client_handler.on_package(self, cmd, args)
|
||||
|
||||
|
||||
async def _game_watcher(ctx: BizHawkClientContext):
|
||||
showed_connecting_message = False
|
||||
showed_connected_message = False
|
||||
showed_no_handler_message = False
|
||||
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
await asyncio.wait_for(ctx.watcher_event.wait(), ctx.watcher_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
ctx.watcher_event.clear()
|
||||
|
||||
try:
|
||||
if ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED:
|
||||
showed_connected_message = False
|
||||
|
||||
if not showed_connecting_message:
|
||||
logger.info("Waiting to connect to BizHawk...")
|
||||
showed_connecting_message = True
|
||||
|
||||
if not await connect(ctx.bizhawk_ctx):
|
||||
continue
|
||||
|
||||
showed_no_handler_message = False
|
||||
|
||||
script_version = await get_script_version(ctx.bizhawk_ctx)
|
||||
|
||||
if script_version != EXPECTED_SCRIPT_VERSION:
|
||||
logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.")
|
||||
disconnect(ctx.bizhawk_ctx)
|
||||
continue
|
||||
|
||||
showed_connecting_message = False
|
||||
|
||||
await ping(ctx.bizhawk_ctx)
|
||||
|
||||
if not showed_connected_message:
|
||||
showed_connected_message = True
|
||||
logger.info("Connected to BizHawk")
|
||||
|
||||
rom_hash = await get_hash(ctx.bizhawk_ctx)
|
||||
if ctx.rom_hash is not None and ctx.rom_hash != rom_hash:
|
||||
if ctx.server is not None:
|
||||
logger.info(f"ROM changed. Disconnecting from server.")
|
||||
await ctx.disconnect(True)
|
||||
|
||||
ctx.auth = None
|
||||
ctx.username = None
|
||||
ctx.rom_hash = rom_hash
|
||||
|
||||
if ctx.client_handler is None:
|
||||
system = await get_system(ctx.bizhawk_ctx)
|
||||
ctx.client_handler = await AutoBizHawkClientRegister.get_handler(ctx, system)
|
||||
|
||||
if ctx.client_handler is None:
|
||||
if not showed_no_handler_message:
|
||||
logger.info("No handler was found for this game")
|
||||
showed_no_handler_message = True
|
||||
continue
|
||||
else:
|
||||
showed_no_handler_message = False
|
||||
logger.info(f"Running handler for {ctx.client_handler.game}")
|
||||
|
||||
except RequestFailedError as exc:
|
||||
logger.info(f"Lost connection to BizHawk: {exc.args[0]}")
|
||||
continue
|
||||
|
||||
# Get slot name and send `Connect`
|
||||
if ctx.server is not None and ctx.username is None:
|
||||
await ctx.client_handler.set_auth(ctx)
|
||||
|
||||
if ctx.auth is None:
|
||||
await ctx.get_username()
|
||||
|
||||
await ctx.send_connect()
|
||||
|
||||
await ctx.client_handler.game_watcher(ctx)
|
||||
|
||||
|
||||
async def _run_game(rom: str):
|
||||
import webbrowser
|
||||
webbrowser.open(rom)
|
||||
|
||||
|
||||
async def _patch_and_run_game(patch_file: str):
|
||||
metadata, output_file = Patch.create_rom_file(patch_file)
|
||||
Utils.async_start(_run_game(output_file))
|
||||
|
||||
|
||||
def launch() -> None:
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = BizHawkClientContext(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
|
||||
if args.patch_file != "":
|
||||
Utils.async_start(_patch_and_run_game(args.patch_file))
|
||||
|
||||
watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher")
|
||||
|
||||
try:
|
||||
await watcher_task
|
||||
except Exception as e:
|
||||
logger.error("".join(traceback.format_exception(e)))
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
await ctx.shutdown()
|
||||
|
||||
Utils.init_logging("BizHawkClient", exception_logger="Client")
|
||||
import colorama
|
||||
colorama.init()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
Reference in New Issue
Block a user