280 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			280 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|   | # BizHawk Client
 | ||
|  | 
 | ||
|  | `BizHawkClient` is an abstract base class for a client that can access the memory of a ROM running in BizHawk. It does | ||
|  | the legwork of connecting Python to a Lua connector script, letting you focus on the loop of checking locations and | ||
|  | making on-the-fly modifications based on updates from the server. It also provides the same experience to users across | ||
|  | multiple games that use it, and was built in response to a growing number of similar but separate bespoke game clients | ||
|  | which are/were largely exclusive to BizHawk anyway. | ||
|  | 
 | ||
|  | It's similar to `SNIClient`, but where `SNIClient` is designed to work for specifically SNES games across different | ||
|  | emulators/hardware, `BizHawkClient` is designed to work for specifically BizHawk across the different systems BizHawk | ||
|  | supports. | ||
|  | 
 | ||
|  | The idea is that `BizHawkClient` connects to and communicates with a Lua script running in BizHawk. It provides an API | ||
|  | that will call BizHawk functions for you to do things like read and write memory. And on an interval, control will be | ||
|  | handed to a function you write for your game (`game_watcher`) which should interact with the game's memory to check what | ||
|  | locations have been checked, give the player items, detect and send deathlinks, etc... | ||
|  | 
 | ||
|  | Table of Contents: | ||
|  | - [Connector Requests](#connector-requests) | ||
|  |     - [Requests that depend on other requests](#requests-that-depend-on-other-requests) | ||
|  | - [Implementing a Client](#implementing-a-client) | ||
|  |     - [Example](#example) | ||
|  | - [Tips](#tips) | ||
|  | 
 | ||
|  | ## Connector Requests
 | ||
|  | 
 | ||
|  | Communication with BizHawk is done through `connector_bizhawk_generic.lua`. The client sends requests to the Lua script | ||
|  | via sockets; the Lua script processes the request and sends the corresponding responses. | ||
|  | 
 | ||
|  | The Lua script includes its own documentation, but you probably don't need to worry about the specifics. Instead, you'll | ||
|  | be using the functions in `worlds/_bizhawk/__init__.py`. If you do need more control over the specific requests being | ||
|  | sent or their order, you can still use `send_requests` to directly communicate with the connector script. | ||
|  | 
 | ||
|  | It's not necessary to use the UI or client context if you only want to interact with the connector script. You can | ||
|  | import and use just `worlds/_bizhawk/__init__.py`, which only depends on default modules. | ||
|  | 
 | ||
|  | Here's a list of the included classes and functions. I would highly recommend looking at the actual function signatures | ||
|  | and docstrings to learn more about each function. | ||
|  | 
 | ||
|  | ``` | ||
|  | class ConnectionStatus | ||
|  | class BizHawkContext | ||
|  | 
 | ||
|  | class NotConnectedError | ||
|  | class RequestFailedError | ||
|  | class ConnectorError | ||
|  | class SyncError | ||
|  | 
 | ||
|  | async def read(ctx, read_list) -> list[bytes] | ||
|  | async def write(ctx, write_list) -> None: | ||
|  | async def guarded_read(ctx, read_list, guard_list) -> (list[bytes] | None) | ||
|  | async def guarded_write(ctx, write_list, guard_list) -> bool | ||
|  | 
 | ||
|  | async def lock(ctx) -> None | ||
|  | async def unlock(ctx) -> None | ||
|  | 
 | ||
|  | async def get_hash(ctx) -> str | ||
|  | async def get_system(ctx) -> str | ||
|  | async def get_cores(ctx) -> dict[str, str] | ||
|  | async def ping(ctx) -> None | ||
|  | 
 | ||
|  | async def display_message(ctx, message: str) -> None | ||
|  | async def set_message_interval(ctx, value: float) -> None | ||
|  | 
 | ||
|  | async def connect(ctx) -> bool | ||
|  | def disconnect(ctx) -> None | ||
|  | 
 | ||
|  | async def get_script_version(ctx) -> int | ||
|  | async def send_requests(ctx, req_list) -> list[dict[str, Any]] | ||
|  | ``` | ||
|  | 
 | ||
|  | `send_requests` is what actually communicates with the connector, and any functions like `guarded_read` will build the | ||
|  | requests and then call `send_requests` for you. You can call `send_requests` yourself for more direct control, but make | ||
|  | sure to read the docs in `connector_bizhawk_generic.lua`. | ||
|  | 
 | ||
|  | A bundle of requests sent by `send_requests` will all be executed on the same frame, and by extension, so will any | ||
|  | helper that calls `send_requests`. For example, if you were to call `read` with 3 items on your `read_list`, all 3 | ||
|  | addresses will be read on the same frame and then sent back. | ||
|  | 
 | ||
|  | It also means that, by default, the only way to run multiple requests on the same frame is for them to be included in | ||
|  | the same `send_requests` call. As soon as the connector finishes responding to a list of requests, it will advance the | ||
|  | frame before checking for the next batch. | ||
|  | 
 | ||
|  | ### Requests that depend on other requests
 | ||
|  | 
 | ||
|  | The fact that you have to wait at least a frame to act on any response may raise concerns. For example, Pokemon | ||
|  | Emerald's save data is at a dynamic location in memory; it moves around when you load a new map. There is a static | ||
|  | variable that holds the address of the save data, so we want to read the static variable to get the save address, and | ||
|  | then use that address in a `write` to send the player an item. But between the `read` that tells us the address of the | ||
|  | save data and the `write` to save data itself, an arbitrary number of frames have been executed, and the player may have | ||
|  | loaded a new map, meaning we've written data to who knows where. | ||
|  | 
 | ||
|  | There are two solutions to this problem. | ||
|  | 
 | ||
|  | 1. Use `guarded_write` instead of `write`. We can include a guard against the address changing, and the script will only | ||
|  | perform the write if the data in memory matches what's in the guard. In the below example, `write_result` will be `True` | ||
|  | if the guard validated and the data was written, and `False` if the guard failed to validate. | ||
|  | 
 | ||
|  | ```py | ||
|  | # Get the address of the save data
 | ||
|  | read_result: bytes = (await _bizhawk.read(ctx, [(0x3001111, 4, "System Bus")]))[0] | ||
|  | save_data_address = int.from_bytes(read_result, "little") | ||
|  | 
 | ||
|  | # Write to `save_data_address` if it hasn't changed
 | ||
|  | write_result: bool = await _bizhawk.guarded_write( | ||
|  |     ctx, | ||
|  |     [(save_data_address, [0xAA, 0xBB], "System Bus")], | ||
|  |     [(0x3001111, read_result, "System Bus")] | ||
|  | ) | ||
|  | 
 | ||
|  | if write_result: | ||
|  |     # The data at 0x3001111 was still the same value as | ||
|  |     # what was returned from the first `_bizhawk.read`, | ||
|  |     # so the data was written. | ||
|  |     ... | ||
|  | else: | ||
|  |     # The data at 0x3001111 has changed since the | ||
|  |     # first `_bizhawk.read`, so the data was not written. | ||
|  |     ... | ||
|  | ``` | ||
|  | 
 | ||
|  | 2. Use `lock` and `unlock` (discouraged if not necessary). When you call `lock`, you tell the emulator to stop advancing | ||
|  | frames and just process requests until it receives an unlock request. This means you can lock, read the address, write | ||
|  | the data, and then unlock on a single frame. **However**, this is _slow_. If you can't get in and get out quickly | ||
|  | enough, players will notice a stutter in the emulation. | ||
|  | 
 | ||
|  | ```py | ||
|  | # Pause emulation
 | ||
|  | await _bizhawk.lock(ctx) | ||
|  | 
 | ||
|  | # Get the address of the save data
 | ||
|  | read_result: bytes = (await _bizhawk.read(ctx, [(0x3001111, 4, "System Bus")]))[0] | ||
|  | save_data_address = int.from_bytes(read_result, "little") | ||
|  | 
 | ||
|  | # Write to `save_data_address`
 | ||
|  | await _bizhawk.write(ctx, [(save_data_address, [0xAA, 0xBB], "System Bus")]) | ||
|  | 
 | ||
|  | # Resume emulation
 | ||
|  | await _bizhawk.unlock(ctx) | ||
|  | ``` | ||
|  | 
 | ||
|  | You should always use `guarded_read` and `guarded_write` instead of locking the emulator if possible. It may be | ||
|  | unreliable, but that's by design. Most of the time you should have no problem giving up and retrying. Data that is | ||
|  | volatile but only changes occasionally is the perfect use case. | ||
|  | 
 | ||
|  | If data is almost guaranteed to change between frames, locking may be the better solution. You can lower the time spent | ||
|  | locked by using `send_requests` directly to include as many requests alongside the `LOCK` and `UNLOCK` requests as | ||
|  | possible. But in general it's probably worth doing some extra asm hacking and designing to make guards work instead. | ||
|  | 
 | ||
|  | ## Implementing a Client
 | ||
|  | 
 | ||
|  | `BizHawkClient` itself is built on `CommonClient` and inspired heavily by `SNIClient`. Your world's client should | ||
|  | inherit from `BizHawkClient` in `worlds/_bizhawk/client.py`. It must implement `validate_rom` and `game_watcher`, and | ||
|  | must define values for `system` and `game`. | ||
|  | 
 | ||
|  | As with the functions and classes in the previous section, I would highly recommend looking at the types and docstrings | ||
|  | of the code itself. | ||
|  | 
 | ||
|  | `game` should be the same value you use for your world definition. | ||
|  | 
 | ||
|  | `system` can either be a string or a tuple of strings. This is the system (or systems) that your client is intended to | ||
|  | handle games on (SNES, GBA, etc.). It's used to prevent validators from running on unknown systems and crashing. The | ||
|  | actual abbreviation corresponds to whatever BizHawk returns from `emu.getsystemid()`. | ||
|  | 
 | ||
|  | `patch_suffix` is an optional `ClassVar` meant to specify the file extensions you want to register. It can be a string | ||
|  | or tuple of strings. When a player clicks "Open Patch" in a launcher, the suffix(es) will be whitelisted in the file | ||
|  | select dialog and they will be associated with BizHawkClient. This does not affect whether the user's computer will | ||
|  | associate the file extension with Archipelago. | ||
|  | 
 | ||
|  | `validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is | ||
|  | running on a system you specified in your `system` class variable. In most cases, that will be a single system and you | ||
|  | can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this | ||
|  | ROM as yours, this is where you should do setup for things like `items_handling`. | ||
|  | 
 | ||
|  | `game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM. | ||
|  | `BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do | ||
|  | its best to make sure you're connected to the connector script before calling your watcher. It runs this loop either | ||
|  | immediately once it receives a message from the server, or a specified amount of time after the last iteration of the | ||
|  | loop finished. | ||
|  | 
 | ||
|  | `validate_rom`, `game_watcher`, and other methods will be passed an instance of `BizHawkClientContext`, which is a | ||
|  | subclass of `CommonContext`. It additionally includes `slot_data` (if you are connected and asked for slot data), | ||
|  | `bizhawk_ctx` (the instance of `BizHawkContext` that you should be giving to functions like `guarded_read`), and | ||
|  | `watcher_timeout` (the amount of time in seconds between iterations of the game watcher loop). | ||
|  | 
 | ||
|  | ### Example
 | ||
|  | 
 | ||
|  | A very simple client might look like this. All addresses here are made up; you should instead be using addresses that | ||
|  | make sense for your specific ROM. The `validate_rom` here tries to read the name of the ROM. If it gets the value it | ||
|  | wanted, it sets a couple values on `ctx` and returns `True`. The `game_watcher` reads some data from memory and acts on | ||
|  | it by sending messages to AP. You should be smarter than this example, which will send `LocationChecks` messages even if | ||
|  | there's nothing new since the last loop. | ||
|  | 
 | ||
|  | ```py | ||
|  | from typing import TYPE_CHECKING | ||
|  | 
 | ||
|  | from NetUtils import ClientStatus | ||
|  | 
 | ||
|  | import worlds._bizhawk as bizhawk | ||
|  | from worlds._bizhawk.client import BizHawkClient | ||
|  | 
 | ||
|  | if TYPE_CHECKING: | ||
|  |     from worlds._bizhawk.context import BizHawkClientContext | ||
|  | 
 | ||
|  | 
 | ||
|  | class MyGameClient(BizHawkClient): | ||
|  |     game = "My Game" | ||
|  |     system = "GBA" | ||
|  |     patch_suffix = ".apextension" | ||
|  | 
 | ||
|  |     async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: | ||
|  |         try: | ||
|  |             # Check ROM name/patch version | ||
|  |             rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(0x100, 6, "ROM")]))[0]).decode("ascii") | ||
|  |             if rom_name != "MYGAME": | ||
|  |                 return False  # Not a MYGAME ROM | ||
|  |         except bizhawk.RequestFailedError: | ||
|  |             return False  # Not able to get a response, say no for now | ||
|  | 
 | ||
|  |         # This is a MYGAME ROM | ||
|  |         ctx.game = self.game | ||
|  |         ctx.items_handling = 0b001 | ||
|  |         ctx.want_slot_data = True | ||
|  | 
 | ||
|  |         return True | ||
|  | 
 | ||
|  |     async def game_watcher(self, ctx: "BizHawkClientContext") -> None: | ||
|  |         try: | ||
|  |             # Read save data | ||
|  |             save_data = await bizhawk.read( | ||
|  |                 ctx.bizhawk_ctx, | ||
|  |                 [(0x3000100, 20, "System Bus")] | ||
|  |             )[0] | ||
|  | 
 | ||
|  |             # Check locations | ||
|  |             if save_data[2] & 0x04: | ||
|  |                 await ctx.send_msgs([{ | ||
|  |                     "cmd": "LocationChecks", | ||
|  |                     "locations": [23] | ||
|  |                 }]) | ||
|  | 
 | ||
|  |             # Send game clear | ||
|  |             if not ctx.finished_game and (save_data[5] & 0x01): | ||
|  |                 await ctx.send_msgs([{ | ||
|  |                     "cmd": "StatusUpdate", | ||
|  |                     "status": ClientStatus.CLIENT_GOAL | ||
|  |                 }]) | ||
|  | 
 | ||
|  |         except bizhawk.RequestFailedError: | ||
|  |             # The connector didn't respond. Exit handler and return to main loop to reconnect | ||
|  |             pass | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Tips
 | ||
|  | 
 | ||
|  | - Make sure your client gets imported when your world is imported. You probably don't need to actually use anything in | ||
|  | your `client.py` elsewhere, but you still have to import the file for your client to register itself. | ||
|  | - When it comes to performance, there are two directions to optimize: | ||
|  |   1. If you need to execute multiple commands on the same frame, do as little work as possible. Only read and write necessary data, | ||
|  |   and if you have to use locks, unlock as soon as it's okay to advance frames. This is probably the obvious one. | ||
|  |   2. Multiple things that don't have to happen on the same frame should be split up if they're likely to be slow. | ||
|  |   Remember, the game watcher runs only a few times per second. Extra function calls on the client aren't that big of a | ||
|  |   deal; the player will not notice if your `game_watcher` is slow. But the emulator has to be done with any given set of | ||
|  |   commands in 1/60th of a second to avoid hiccups (faster still if your players use speedup). Too many reads of too much | ||
|  |   data at the same time is more likely to cause a bad user experience. | ||
|  | - Your `game_watcher` will be called regardless of the status of the client's connection to the server. Double-check the | ||
|  | server connection before trying to interact with it. | ||
|  | - By default, the player will be asked to provide their slot name after connecting to the server and validating, and | ||
|  | that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to | ||
|  | set it automatically based on data in the ROM or on your client instance. | ||
|  | - You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a | ||
|  | subclass of `CommonContext` and its API. | ||
|  | - You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at | ||
|  | the top of the file will probably cause a circular dependency. | ||
|  | - Your game's system may have multiple usable cores in BizHawk. You can use `get_cores` to try to determine which one is | ||
|  | currently loaded (it's the best we can do). Some cores may differ in the names of memory domains. It's good to check all | ||
|  | the available cores to find differences before your users do. | ||
|  | - The connector script includes a DEBUG variable that you can use to log requests/responses. (Be aware that as the log | ||
|  | grows in size in BizHawk, it begins to stutter while trying to print it.) |