#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#
"""
The core module of the emulator
"""
import heapq
import os
import re
import time
from operator import index
from pathlib import Path
import numpy as np
from itertools import cycle
from pyboy.api.constants import TILES, TILES_CGB
from pyboy.api.gameshark import GameShark
from pyboy.api.memory_scanner import MemoryScanner
from pyboy.api.screen import Screen
from pyboy.api.sgb import SGB
from pyboy.api.sound import Sound
from pyboy.api.tilemap import TileMap
from pyboy.api.rumble import Rumble
from pyboy.logging import get_logger
from pyboy.logging import log_level as _log_level
from pyboy.plugins.manager import PluginManager, parser_arguments
from pyboy.utils import (
IntIOWrapper,
PyBoyException,
PyBoyInvalidInputException,
PyBoyInvalidOperationException,
PyBoyOutOfBoundsException,
WindowEvent,
cython_compiled,
OPCODE_BRK,
)
try:
import cython
except ImportError:
class _mock:
def __enter__(self):
pass
def __exit__(self, *args):
pass
exec(
"""
class cython:
gil = _mock()
nogil = _mock()
""",
globals(),
locals(),
)
from .api import Sprite, Tile, constants
from .core.mb import Motherboard
logger = get_logger(__name__)
SPF = 1 / 60.0 # inverse FPS (frame-per-second)
DMG_PALETTES = {
"SameBoy DMG": (0xC6DE8C, 0x84A563, 0x396139, 0x081810),
"Classic Green": (0x9BBC0F, 0x8BAC0F, 0x306230, 0x0F380F),
"Parchment": (0xE0DBCD, 0xA89F94, 0x706B64, 0x2B2B26),
"Mossy": (0xC4CFA1, 0x8B956D, 0x4D533C, 0x1F1F1C),
"Grey": (0xFFFFFF, 0x999999, 0x555555, 0x000000),
}
defaults = {
"color_palette": DMG_PALETTES["Grey"],
"cgb_color_palette": (
(0xFFFFFF, 0x7BFF31, 0x0063C5, 0x000000),
(0xFFFFFF, 0xFF8484, 0x943A3A, 0x000000),
(0xFFFFFF, 0xFF8484, 0x943A3A, 0x000000),
),
"scale": 3,
"window": "SDL2",
"log_level": "WARNING",
}
[docs]
class PyBoy:
[docs]
def __init__(
self,
gamerom,
*,
ram_file=None,
rtc_file=None,
window=defaults["window"],
scale=defaults["scale"],
symbols=None,
bootrom=None,
sound_volume=100,
sound_emulated=True,
sound_sample_rate=None,
cgb=None,
gameshark=None,
no_input=False,
log_level=defaults["log_level"],
color_palette=defaults["color_palette"],
cgb_color_palette=defaults["cgb_color_palette"],
title_status=False,
serial_shared_memory=None,
serial_interrupt_based=False,
sgb_border=False,
**kwargs,
):
"""
PyBoy is loadable as an object in Python. This means, it can be initialized from another script, and be
controlled and probed by the script. It is supported to spawn multiple emulators, just instantiate the class
multiple times.
A range of methods are exposed, which should allow for complete control of the emulator. Please open an issue on
GitHub, if other methods are needed for your projects. Take a look at the files in `examples/` for a crude
"bots", which interact with the game.
Only the `gamerom` argument is required.
If `gamerom` is a filepath and `ram_file` or `rtc_file` is not provided, PyBoy looks for matching `.ram` and
`.rtc` files next to the game ROM. If this is not wanted, provide file-like objects explicitly.
Example:
```python
>>> pyboy = PyBoy('game_rom.gb')
>>> for _ in range(60): # Use 'while True:' for infinite
... pyboy.tick()
True...
>>> pyboy.stop()
```
Args:
gamerom (str, os.PathLike, or binary file-like object): Path to a Game Boy or Game Boy Color ROM, or an
already-open binary file.
ram_file (binary file-like object or None): Existing cartridge RAM data to load. When omitted for a
path-based ROM, PyBoy looks for `<gamerom>.ram`; pass a destination to `stop` to save to a stream.
rtc_file (binary file-like object or None): Existing cartridge RTC data to load. When omitted for a
path-based ROM, PyBoy looks for `<gamerom>.rtc`; pass a destination to `stop` to save to a stream.
window (str): Window backend: "SDL2", "OpenGL", "GLFW", or "null". "headless" and "dummy" are aliases
for "null".
scale (int): Window scale factor; it does not affect API image dimensions.
symbols (str or os.PathLike or None): Path to a `.sym` or `.map` symbol file.
bootrom (str or os.PathLike or None): Path to a boot ROM to use instead of the built-in boot ROM.
sound_volume (int): Sound volume from 0 to 100. Defaults to 100.
sound_emulated (bool): Whether to emulate sound. Disabling it also disables sound sampling.
sound_sample_rate (int or None): Positive sound sample rate in Hz. Defaults to 48,000 Hz.
cgb (bool or None): Force Game Boy Color mode (`True`) or original Game Boy mode (`False`); `None`
auto-detects the mode from the ROM and boot ROM.
gameshark (str or None): Comma-separated GameShark codes to apply at startup.
no_input (bool): Disable user input, mainly for autonomous use.
log_level (str): One of "CRITICAL", "ERROR", "WARNING", "INFO", or "DEBUG".
color_palette (sequence[int]): Four 24-bit RGB colors for the DMG palette.
cgb_color_palette (sequence[sequence[int]]): Three palettes of four 24-bit RGB colors, in background,
object palette 0, and object palette 1 order.
title_status (bool): Show performance status in the window title.
sgb_border (bool): Enable SGB processing and border rendering. The border is drawn by the
SDL2 and GLFW windows; other windows get SGB processing without border display.
serial_shared_memory (object or None): Shared-memory link buffer used to connect two emulators. The
object must provide `read`, `write`, and `synchronize` methods.
serial_interrupt_based (bool): Use interrupt-based serial transfer when `serial_shared_memory` is set.
autopause (bool): Enable auto-pausing when window looses focus [plugin: AutoPause]
breakpoints (str): Add breakpoints on start-up (internal use) [plugin: DebugPrompt]
printer (bool): Enable Game Boy Printer emulation [plugin: GameBoyPrinter]
printer_output (str): Output directory for printed images (default: current directory) [plugin: GameBoyPrinter]
record_input (bool): Record user input and save to a file (internal use) [plugin: RecordReplay]
rewind (bool): Enable rewind function [plugin: Rewind]
.. plugin kwargs end
Plugin-specific options are listed here and can be shown with `pyboy --help`.
"""
self.initialized = False
self.no_input = no_input
_log_level(log_level)
logger.debug(f"Cython compilation status: {cython_compiled}")
if "bootrom_file" in kwargs:
logger.error(
"Deprecated use of 'bootrom_file'. Use 'bootrom' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0"
)
bootrom = kwargs.pop("bootrom_file")
if "window_type" in kwargs:
logger.error(
"Deprecated use of 'window_type'. Use 'window' keyword argument instead. https://github.com/Baekalfen/PyBoy/wiki/Migrating-from-v1.x.x-to-v2.0.0"
)
window = kwargs.pop("window_type")
if window not in ["SDL2", "OpenGL", "GLFW", "null", "headless", "dummy"]:
raise KeyError(f'Unknown window type: {window}. Use "SDL2", "OpenGL", "GLFW", or "null"')
kwargs["window"] = window
kwargs["scale"] = scale
kwargs["sgb_border"] = sgb_border
randomize = kwargs.pop("randomize", False) # Undocumented feature
for k, v in defaults.items():
if k not in kwargs:
kwargs[k] = v
if gamerom is None:
raise FileNotFoundError("No game ROM provided!")
gamerom_file = None
self.gamerom = None
gamerom_file_handled = False
ram_file_handled = False
rtc_file_handled = False
if isinstance(gamerom, (str, Path)):
self.gamerom = str(gamerom)
try:
gamerom_file = open(self.gamerom, "rb")
gamerom_file_handled = True
if ram_file is None:
try:
ram_file = open(self.gamerom + ".ram", "rb")
ram_file_handled = True
except FileNotFoundError:
pass
if rtc_file is None:
try:
rtc_file = open(self.gamerom + ".rtc", "rb")
rtc_file_handled = True
except FileNotFoundError:
pass
except FileNotFoundError:
raise FileNotFoundError(f"ROM file {gamerom} was not found!")
except Exception:
raise
elif hasattr(gamerom, "read"):
gamerom_file = gamerom
else:
raise PyBoyInvalidInputException("Provided game ROM cannot be used. Expected str, Path or file-like object")
self.rom_symbols = {}
self.rom_symbols_inverse = {}
if symbols is not None:
if not os.path.isfile(symbols):
raise FileNotFoundError(f"Symbols file {symbols} was not found!")
self.symbols_file = symbols
self._load_symbols()
# Backwards compatibility
# Setting volume if True, but we don't disable emulation if False/None.
if kwargs.pop("sound", None):
sound_volume = 100
logger.error(
'Deprecated use of "sound" on PyBoy constructor. Use "sound_volume" or "sound_emulated" instead.'
)
if not (0 <= sound_volume <= 100):
raise PyBoyInvalidInputException("Sound volume has to be between 0 and 100.")
try:
self.mb = Motherboard(
gamerom_file,
ram_file,
rtc_file,
bootrom,
color_palette,
cgb_color_palette,
sound_volume,
sound_emulated,
sound_sample_rate,
cgb,
randomize=randomize,
serial_shared_memory=serial_shared_memory,
serial_interrupt_based=serial_interrupt_based,
)
finally:
# Close the files we opened -- i.e. not passed from args
if gamerom_file_handled:
gamerom_file.close()
if ram_file_handled:
ram_file.close()
if rtc_file_handled:
rtc_file.close()
# Active palette detection
self._palette_cycle = cycle(DMG_PALETTES.items())
# Validate all kwargs
plugin_manager_keywords = []
for x in parser_arguments():
if not x:
continue
plugin_manager_keywords.extend(z.strip("-").replace("-", "_") for y in x for z in y[:-1])
for k, v in kwargs.items():
if k not in defaults and k not in plugin_manager_keywords:
logger.critical("Unknown keyword argument: %s", k)
raise KeyError(f"Unknown keyword argument: {k}")
# Performance measures
self.avg_tick = 0
self.avg_emu = 0
# Absolute frame count of the emulation
self.frame_count = 0
self.set_emulation_speed(1)
self.paused = False
self.events = []
self.queued_input = []
self.quitting = False
self.stopped = False
self.window_title = ""
self.title_status = title_status
self.sgb_border = sgb_border
# If SGB border is enabled, activate SGB processing
if self.sgb_border:
try:
sgb_module = self.mb.sgb
if self.mb.sgb_capable:
sgb_module.enabled = True
sgb_module.state.sgb_detected = True
sgb_module.state.border_enabled = True
logger.debug("SGB border forced enabled via --sgb-border flag")
else:
logger.warning("SGB border requested but cartridge is not SGB-compatible")
except Exception as e:
logger.debug(f"Could not enable SGB border: {e}")
###################
# API attributes
self.screen = Screen(self.mb)
"""
This attribute provides a `pyboy.api.screen.Screen` object for reading the screen buffer in
a variety of formats.
It's also here you can find the screen position (SCX, SCY, WX, WY) for each scan line in the screen buffer. See
`pyboy.api.screen.Screen.tilemap_position_list` for more information.
Example:
```python
>>> pyboy.screen.image.show()
>>> pyboy.screen.ndarray.shape
(144, 160, 4)
>>> pyboy.screen.raw_buffer_format
'RGBA'
```
NOTE: See `PyBoy.sound` to get the sound buffer.
Returns
-------
`pyboy.api.screen.Screen`:
A Screen object with helper functions for reading the screen buffer.
"""
self.sgb = SGB(self.mb)
"""
This attribute provides a `pyboy.api.sgb.SGB` object for accessing Super Game Boy features,
such as SGB detection status and border control.
Returns
-------
`pyboy.api.sgb.SGB`:
An SGB object with helper functions for accessing Super Game Boy features.
"""
self.sound = Sound(self.mb)
"""
This attribute provides a `pyboy.api.sound.Sound` object for reading the sound buffer of the
latest screen frame (see `PyBoy.screen`).
Example:
```python
>>> pyboy.sound.ndarray.shape[1] # Number of stereo channels
2
>>> pyboy.sound.ndarray
array([[0, 0],
[0, 0],
...
[0, 0],
[0, 0]], dtype=int8)
```
Returns
-------
`pyboy.api.sound.Sound`:
A Sound object with helper functions for accessing the sound buffer.
"""
self.rumble = Rumble(self.mb)
"""
This attribute provides a `pyboy.api.rumble.Rumble` object for reading the current cartridge rumble state.
Returns
-------
`pyboy.api.rumble.Rumble`:
Object exposing cartridge rumble support and state.
"""
self.memory = PyBoyMemoryView(self.mb)
"""
Provides a `pyboy.PyBoyMemoryView` object for reading and writing the memory space of the Game Boy.
For a more comprehensive description, see the `pyboy.PyBoyMemoryView` class.
Example:
```python
>>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from ROM bank 0
[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
>>> pyboy.memory[1, 0x2000] = 12 # Override address 0x2000 from ROM bank 1 with the value 12
>>> pyboy.memory[0xC000] = 1 # Write to address 0xC000 with value 1
```
"""
self.register_file = PyBoyRegisterFile(self.mb.cpu)
"""
Provides a `pyboy.PyBoyRegisterFile` object for reading and writing the CPU registers of the Game Boy.
The register file is best used inside the callback registered with `PyBoy.hook_register`, as `PyBoy.tick` doesn't return at a specific point.
For a more comprehensive description, see the `pyboy.PyBoyRegisterFile` class.
Example:
```python
>>> def my_callback(register_file):
... print("Register A:", register_file.A)
>>> pyboy.hook_register(0, 0x100, my_callback, pyboy.register_file)
>>> pyboy.tick(70)
Register A: 1
True
```
"""
self.memory_scanner = MemoryScanner(self)
"""
Provides a `pyboy.api.memory_scanner.MemoryScanner` object for locating addresses of interest in the memory space
of the Game Boy. This might require some trial and error. Values can be represented in memory in surprising ways.
_Open an issue on GitHub if you need finer control, and we will take a look at it._
Example:
```python
>>> pyboy.memory[0xC000] = 4
>>> pyboy.memory_scanner.scan_memory(4, start_addr=0xC000, end_addr=0xC000)
[49152]
>>> pyboy.memory[0xC000] = 8
>>> from pyboy.api.memory_scanner import DynamicComparisonType
>>> addresses = pyboy.memory_scanner.rescan_memory(8, DynamicComparisonType.MATCH)
>>> print(addresses)
[49152]
```
"""
self.tilemap_background = TileMap(self, self.mb, "BACKGROUND")
"""
The Game Boy uses two tile maps at the same time to draw graphics on the screen. This attribute provides the
background tile map. The game chooses whether it uses the low or the high tile map.
Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html).
Example:
```
>>> pyboy.tilemap_background[8,8]
1
>>> pyboy.tilemap_background[7:12,8]
[0, 1, 0, 1, 0]
>>> pyboy.tilemap_background[7:12,8:11]
[[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]]
```
Returns
-------
`pyboy.api.tilemap.TileMap`:
A TileMap object for the tile map.
"""
self.tilemap_window = TileMap(self, self.mb, "WINDOW")
"""
The Game Boy uses two tile maps at the same time to draw graphics on the screen. This attribute provides the
window tile map. The game chooses whether it uses the low or the high tile map.
Read more details about it, in the [Pan Docs](https://gbdev.io/pandocs/Tile_Maps.html).
Example:
```
>>> pyboy.tilemap_window[8,8]
1
>>> pyboy.tilemap_window[7:12,8]
[0, 1, 0, 1, 0]
>>> pyboy.tilemap_window[7:12,8:11]
[[0, 1, 0, 1, 0], [0, 2, 3, 4, 5], [0, 0, 6, 0, 0]]
```
Returns
-------
`pyboy.api.tilemap.TileMap`:
A TileMap object for the tile map.
"""
self.cartridge_title = self.mb.cartridge.gamename
"""
The title bytes read from the cartridge header up to the first NUL. The header field overlaps other metadata;
PyBoy reads up to 14 bytes for CGB-compatible cartridges or 15 bytes otherwise.
Example:
```python
>>> pyboy.cartridge_title # Title of PyBoy's default ROM
'DEFAULT-ROM'
```
Returns
-------
str :
Game title
"""
self._hooks = {}
self._plugin_manager = PluginManager(self, self.mb, kwargs)
"""
Returns
-------
`pyboy.plugins.manager.PluginManager`:
Object for handling plugins in PyBoy
"""
self.game_wrapper = self._plugin_manager.gamewrapper()
"""
Provides an instance of a game-specific or generic wrapper. The game is detected by the cartridge's hard-coded
game title (see `pyboy.PyBoy.cartridge_title`).
If a game-specific wrapper is not found, a generic wrapper will be returned.
To get more information, find the wrapper for your game in `pyboy.plugins`.
Example:
```python
>>> pyboy.game_wrapper.start_game()
>>> pyboy.game_wrapper.reset_game()
```
Returns
-------
`pyboy.plugins.base_plugin.PyBoyGameWrapper`:
A game-specific wrapper object.
"""
self.gameshark = GameShark(self.memory)
"""
Provides an instance of the `pyboy.api.gameshark.GameShark` handler. This allows you to inject GameShark-based cheat codes.
Example:
```python
>>> pyboy.gameshark.add("010138CD")
>>> pyboy.gameshark.remove("010138CD")
>>> pyboy.gameshark.clear_all()
```
"""
if gameshark:
for code in gameshark.split(","):
self.gameshark.add(code.strip())
self.initialized = True
def _tick(self, render, sound):
if self.stopped:
return False
self._handle_events(self.events)
if not self.paused:
self.gameshark.tick()
self.mb.lcd.frame_done = False
self.mb.lcd.disable_renderer = not render
self.mb.sound.disable_sampling = not sound
self.mb.sound.clear_buffer()
# Reenter mb.tick until we eventually get a clean exit without breakpoints
while self.mb.tick() and (not self.quitting):
# Breakpoint reached
# NOTE: Potentially reinject breakpoint that we have now stepped passed
self.mb.breakpoint_reinject()
with cython.gil:
# NOTE: PC has not been incremented when hitting breakpoint!
breakpoint_meta = self.mb.breakpoint_reached()
if breakpoint_meta != (-1, -1, -1):
bank, addr, _ = breakpoint_meta
self.mb.breakpoint_remove(bank, addr)
self.mb.breakpoint_singlestep_latch = 0
if not self._handle_hooks():
self._plugin_manager.handle_breakpoint()
else:
if self.mb.breakpoint_singlestep_latch:
if not self._handle_hooks():
self._plugin_manager.handle_breakpoint()
# Keep singlestepping on, if that's what we're doing
self.mb.breakpoint_singlestep = self.mb.breakpoint_singlestep_latch
self.frame_count += 1
self._post_handle_events()
return not self.quitting
[docs]
def tick(self, count=1, render=True, sound=True):
"""
Progresses the emulator ahead by `count` frame(s).
To run the emulator in real time, it needs to process about 60 frames per second. This function blocks for
roughly 16.7 ms per frame unless you change the limit with `PyBoy.set_emulation_speed`.
If you need finer control than 1 frame, have a look at `PyBoy.hook_register` to inject code at a specific point
in the game.
Setting `render` to `True` will make PyBoy render the screen for *the last frame* of this tick. This can be seen
as a type of "frameskipping" optimization.
For AI training, it is advisable to use as high a count as practical, as it will otherwise reduce performance
substantially. While setting `render` to `False`, you can still access the `PyBoy.game_area` to get a simpler
representation of the game.
If `render` was enabled, use `pyboy.api.screen.Screen` to get a NumPy buffer or raw memory buffer.
Set `sound` to `False` to skip sampling audio for the final frame in this call.
Example:
```python
>>> pyboy.tick() # Progress 1 frame with rendering
True
>>> pyboy.tick(1) # Progress 1 frame with rendering
True
>>> pyboy.tick(60, False) # Progress 60 frames *without* rendering
True
>>> pyboy.tick(60, True) # Progress 60 frames and render *only the last frame*
True
>>> for _ in range(60): # Progress 60 frames and render every frame
... if not pyboy.tick(1, True):
... break
>>>
```
Args:
count (int): Non-negative number of frames to process. Defaults to 1.
render (bool): Whether to render the final frame. Defaults to True.
sound (bool): Whether to sample audio for the final frame. Defaults to True.
Returns:
bool: False if emulation has ended; otherwise True.
Raises:
PyBoyInvalidInputException: If `count` is not a non-negative integer.
"""
if self.stopped:
raise PyBoyInvalidOperationException("Emulator is stopped")
try:
count = index(count)
except TypeError:
raise PyBoyInvalidInputException("count must be a non-negative integer") from None
if count < 0:
raise PyBoyInvalidInputException("count must be a non-negative integer")
_count = count
running = False
t_start = time.perf_counter_ns()
with cython.nogil:
while count != 0:
# Only render screen and sample sound on last tick to improve performance
_render = render and count == 1
_sound = sound and count == 1
running = self._tick(_render, _sound)
count -= 1
t_tick = time.perf_counter_ns()
self._post_tick()
t_post = time.perf_counter_ns()
if _count > 0:
nsecs = t_tick - t_start
self.avg_tick = 0.9 * (self.avg_tick / _count) + (0.1 * nsecs / 1_000_000_000)
nsecs = t_post - t_start
self.avg_emu = 0.9 * (self.avg_emu / _count) + (0.1 * nsecs / 1_000_000_000)
return running
def _cycle_palette(self):
"""Cycles to the next DMG palette."""
palette_name, new_palette = next(self._palette_cycle)
try:
self.set_color_palette(new_palette)
except PyBoyInvalidOperationException as ex:
logger.warning("Error cycling palette: %s", ex)
return
logger.info("Palette: %s", palette_name)
def _handle_events(self, events):
if not self.no_input:
# This feeds events into the tick-loop from the window. There might already be events in the list from the API.
events = self._plugin_manager.handle_events(events)
for event in events:
if event == WindowEvent.QUIT:
self.quitting = True
elif event == WindowEvent.RELEASE_SPEED_UP:
# Switch between unlimited and 1x real-time emulation speed
self.target_emulationspeed = int(bool(self.target_emulationspeed) ^ True)
logger.debug("Speed limit: %d", self.target_emulationspeed)
elif event == WindowEvent.STATE_SAVE:
if self.gamerom:
with open(self.gamerom + ".state", "wb") as f:
self.mb.save_state(IntIOWrapper(f))
else:
logger.error("Failed to save game state. PyBoy is loaded without a filepath.")
elif event == WindowEvent.STATE_LOAD:
if self.gamerom:
state_path = self.gamerom + ".state"
if not os.path.isfile(state_path):
logger.error("State file not found: %s", state_path)
continue
with open(state_path, "rb") as f:
self.mb.load_state(IntIOWrapper(f))
else:
logger.error("Failed to load game state. PyBoy is loaded without a filepath.")
elif event == WindowEvent.PASS:
pass # Used in place of None in Cython, when key isn't mapped to anything
elif event == WindowEvent.PAUSE_TOGGLE:
if self.paused:
self._unpause()
else:
self._pause()
elif event == WindowEvent.PAUSE:
self._pause()
elif event == WindowEvent.UNPAUSE:
self._unpause()
elif event == WindowEvent._INTERNAL_RENDERER_FLUSH:
self._plugin_manager._post_tick_windows()
elif event == WindowEvent.CYCLE_PALETTE:
self._cycle_palette()
else:
self.mb.buttonevent(event)
def _pause(self):
if self.paused:
return
self.paused = True
self.save_target_emulationspeed = self.target_emulationspeed
self.target_emulationspeed = 1
logger.debug("Emulation paused!")
self._update_window_title()
self._plugin_manager.paused(True)
def _unpause(self):
if not self.paused:
return
self.paused = False
self.target_emulationspeed = self.save_target_emulationspeed
logger.debug("Emulation unpaused!")
self._update_window_title()
self._plugin_manager.paused(False)
def _post_tick(self):
# Process SGB VRAM transfer countdown (3-frame delay before reading screen buffer)
if self.mb.sgb.enabled:
self.mb.sgb.tick_frame()
# Fix buggy PIL. They will copy our image buffer and destroy the
# reference on some user operations like .save().
if self.screen.image and not self.screen.image.readonly:
self.screen._set_image()
if self.frame_count % 60 == 0:
self._update_window_title()
self._plugin_manager.post_tick()
self._plugin_manager.frame_limiter(self.target_emulationspeed)
def _post_handle_events(self):
# Prepare an empty list, as the API might be used to send in events between ticks
self.events = []
while self.queued_input and self.frame_count == self.queued_input[0][0]:
_, _event = heapq.heappop(self.queued_input)
self.events.append(WindowEvent(_event))
def _update_window_title(self):
if self.title_status:
self.window_title = f"CPU/frame: {(self.avg_tick) / SPF * 100:0.2f}%"
self.window_title += f' Emulation: x{(round(SPF / self.avg_emu) if self.avg_emu > 0 else "INF")}'
else:
self.window_title = "PyBoy"
if self.paused:
self.window_title += " [PAUSED]"
self.window_title += self._plugin_manager.window_title()
self._plugin_manager._set_title()
def __del__(self):
self.stop(save=False)
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self.stop()
def _quit(self):
self.quitting = True
[docs]
def stop(self, save=True, ram_file=None, rtc_file=None):
"""
Gently stops the emulator and all sub-modules.
If `save` is True, battery-backed cartridge RAM and RTC data are written to the supplied file-like objects, or
to `.ram` and `.rtc` files next to a path-based ROM when no objects are supplied.
For a ROM opened from a file-like object, provide `ram_file` and `rtc_file` destinations as needed.
Example:
```python
>>> pyboy.stop() # Stop emulator and save game progress (cartridge RAM)
>>> pyboy.stop(False) # Stop emulator and discard game progress (cartridge RAM)
>>> import io
>>> sav = io.BytesIO()
>>> pyboy.stop(ram_file=sav) # Stop emulator and save game progress (cartridge RAM)
```
Args:
save (bool): Whether to save battery-backed cartridge data while stopping. Defaults to True.
ram_file (binary file-like object or None): Destination for cartridge RAM data.
rtc_file (binary file-like object or None): Destination for RTC data, if the cartridge has an RTC.
"""
if self.initialized and not self.stopped:
logger.debug("###########################")
logger.debug("# Emulator is turning off #")
logger.debug("###########################")
self._plugin_manager.stop()
# Battery implies saving RAM
ram_file_handled = False
rtc_file_handled = False
if save and self.mb.cartridge.battery and ram_file is None:
ram_file = open(self.gamerom + ".ram", "w+b")
ram_file_handled = True
if save and self.mb.cartridge.rtc_enabled and rtc_file is None:
rtc_file = open(self.gamerom + ".rtc", "w+b")
rtc_file_handled = True
self.mb.stop(save, ram_file, rtc_file)
if ram_file_handled:
ram_file.close()
if rtc_file_handled:
rtc_file.close()
self.stopped = True
###################################################################
# Scripts and bot methods
#
[docs]
def save_state(self, file_like_object):
"""
Saves the complete state of the emulator. It can be called at any time, and enable you to revert any progress in
a game.
You can either save it to a file, or in-memory. The following two examples will provide the file handle in each
case. Remember to `seek` the in-memory buffer to the beginning before calling `PyBoy.load_state`:
```python
>>> # Save to file
>>> with open("state_file.state", "wb") as f:
... pyboy.save_state(f)
>>>
>>> # Save to memory
>>> import io
>>> with io.BytesIO() as f:
... f.seek(0)
... pyboy.save_state(f)
0
```
Args:
file_like_object (io.BufferedIOBase): A file-like object for which to write the emulator state.
"""
if isinstance(file_like_object, str):
raise PyBoyInvalidInputException(
"String not allowed. Did you specify a filepath instead of a file-like object?"
)
if file_like_object.__class__.__name__ == "TextIOWrapper":
raise PyBoyInvalidInputException("Text file not allowed. Did you specify open(..., 'wb')?")
self.mb.save_state(IntIOWrapper(file_like_object))
[docs]
def load_state(self, file_like_object):
"""
Restores the complete state of the emulator. It can be called at any time, and enable you to revert any progress
in a game.
You can either load it from a file, or from memory. See `PyBoy.save_state` for how to save the state, before you
can load it here.
To load a file, remember to load it as bytes:
```python
>>> # Load file
>>> with open("state_file.state", "rb") as f:
... pyboy.load_state(f)
>>>
```
Args:
file_like_object (io.BufferedIOBase): A file-like object for which to read the emulator state.
"""
if isinstance(file_like_object, str):
raise PyBoyInvalidInputException(
"String not allowed. Did you specify a filepath instead of a file-like object?"
)
if file_like_object.__class__.__name__ == "TextIOWrapper":
raise PyBoyInvalidInputException("Text file not allowed. Did you specify open(..., 'rb')?")
self.mb.load_state(IntIOWrapper(file_like_object))
[docs]
def game_area_dimensions(self, x, y, width, height, follow_scrolling=True):
"""
If using the generic game wrapper (see `pyboy.PyBoy.game_wrapper`), you can use this to set the section of the
tilemaps to extract. This will default to the entire tilemap.
Example:
```python
>>> pyboy.game_wrapper.shape
(32, 32)
>>> pyboy.game_area_dimensions(2, 2, 10, 18, False)
>>> pyboy.game_wrapper.shape
(10, 18)
```
Args:
x (int): Left column of the game area in tile-map coordinates.
y (int): Top row of the game area in tile-map coordinates.
width (int): Width of the game area in tiles.
height (int): Height of the game area in tiles.
follow_scrolling (bool): Whether to adjust the area for SCX/SCY scrolling.
"""
self.game_wrapper._set_dimensions(x, y, width, height, follow_scrolling)
[docs]
def game_area_collision(self):
"""
Some game wrappers define a collision map. Check if your game wrapper has this feature implemented: `pyboy.plugins`.
The output will be unique for each game wrapper.
Example:
```python
>>> # This example show nothing, but a supported game will
>>> pyboy.game_area_collision()
array([[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint32)
```
Returns:
numpy.ndarray: A two-dimensional collision map. Only game wrappers that implement collision data support
this method.
Raises:
AttributeError: If the active game wrapper does not implement collision data.
"""
return self.game_wrapper.game_area_collision()
[docs]
def game_area_mapping(self, mapping, sprite_offset=0):
"""
Define custom mappings for tile identifiers in the game area.
Example of custom mapping:
```python
>>> from pyboy.api.constants import TILES
>>> mapping = [x for x in range(TILES)] # 1:1 mapping of 384 tiles
>>> mapping[0] = 0 # Map tile identifier 0 -> 0
>>> mapping[1] = 0 # Map tile identifier 1 -> 0
>>> mapping[2] = 0 # Map tile identifier 2 -> 0
>>> mapping[3] = 0 # Map tile identifier 3 -> 0
>>> pyboy.game_area_mapping(mapping, 1000)
```
Some game wrappers will supply mappings as well. See the specific documentation for your game wrapper:
`pyboy.plugins`.
```python
>>> pyboy.game_area_mapping(pyboy.game_wrapper.mapping_one_to_one, 0)
```
Args:
mapping (list, ndarray, or None): A mapping with 384 (DMG) or 768 (CGB) entries. `None` resets to an
identity mapping.
sprite_offset (int): Value added to mapped tile IDs used for sprites. Defaults to 0.
"""
if mapping is None:
mapping = [x for x in range(TILES_CGB)]
assert isinstance(sprite_offset, int)
assert isinstance(mapping, (np.ndarray, list))
assert len(mapping) == TILES or len(mapping) == TILES_CGB
self.game_wrapper.game_area_mapping(mapping, sprite_offset)
[docs]
def game_area(self):
"""
Use this method to get a matrix of the "game area" of the screen. This view is simplified to be perfect for
machine learning applications.
The layout will vary from game to game. Below is an example from Tetris:
Example:
```python
>>> pyboy.game_area()
array([[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 130, 130, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 130, 130, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47],
[ 47, 47, 47, 47, 47, 47, 47, 47, 47, 47]], dtype=uint32)
```
If you want a "compressed", "minimal" or raw mapping of tiles, you can change the mapping using
`pyboy.PyBoy.game_area_mapping`. Either you'll have to supply your own mapping, or you can find one
that is built-in with the game wrapper plugin for your game. See `pyboy.PyBoy.game_area_mapping`.
Returns
-------
numpy.ndarray:
Two-dimensional array of mapped tile identifiers with dtype `numpy.uint32` and shape
`(game_wrapper.shape[1], game_wrapper.shape[0])`, i.e. `(height, width)`.
"""
return self.game_wrapper.game_area()
[docs]
def set_color_palette(self, palette):
"""
Set the color palette of DMG games.
Example:
```python
>>> pyboy.set_color_palette((0x9BBC0F, 0x8BAC0F, 0x306230, 0x0F380F))
```
Args:
palette (sequence[int]): Four 24-bit RGB colors, ordered from lightest to darkest.
Raises:
PyBoyInvalidOperationException: If PyBoy is running in CGB mode.
"""
if self.mb.cgb:
raise PyBoyInvalidOperationException("Palette change is only available in DMG mode")
for palette_reg in [self.mb.lcd.BGP, self.mb.lcd.OBP0, self.mb.lcd.OBP1]:
palette_reg.set_palette_colors(palette)
def _serial(self):
"""
Provides all data that has been sent over the serial port since last call to this function.
Returns
-------
str :
Buffer data
"""
return self.mb.getserial()
[docs]
def printer_image(self):
"""
Returns the last image printed by the Game Boy Printer, if printer emulation is enabled.
Returns
-------
PIL.Image.Image or None :
The last printed image as a PIL Image, or None if no image has been printed or the printer is not enabled.
"""
if self._plugin_manager.game_boy_printer_enabled:
return self._plugin_manager.game_boy_printer.get_image()
return None
[docs]
def set_emulation_speed(self, target_speed):
"""
Set the target emulation speed. Timing may become less accurate at high target speeds.
The speed is defined as a multiple of real-time. I.e `target_speed=2` is double speed.
A `target_speed` of `0` means unlimited. I.e. fastest possible execution.
Due to backwards compatibility, the null window starts at unlimited speed (i.e. `target_speed=0`), while
others start at realtime (i.e. `target_speed=1`).
Example:
```python
>>> pyboy.tick() # Delays 16.67ms
True
>>> pyboy.set_emulation_speed(0) # Disable limit
>>> pyboy.tick() # As fast as possible
True
```
Args:
target_speed (int): Target speed as a multiple of real time. Use 0 for unlimited speed, or a positive
integer for a real-time multiple.
"""
if target_speed > 5:
logger.warning("The emulation speed might not be accurate when speed-target is higher than 5")
self.target_emulationspeed = target_speed
# sdcc/sdld linker map line, e.g.: "00000A00 _board G"
_MAP_SYMBOL_RE = re.compile(r"^\s*([0-9A-Fa-f]{8})\s+(\S+)\s+\S*\s*$")
def _load_symbols(self):
if self.rom_symbols:
return self.rom_symbols
gamerom_paths = []
if self.gamerom:
gamerom_file_no_ext, rom_ext = os.path.splitext(self.gamerom)
gamerom_paths = [
gamerom_file_no_ext + ".sym",
gamerom_file_no_ext + rom_ext + ".sym",
gamerom_file_no_ext + ".map",
gamerom_file_no_ext + rom_ext + ".map",
]
for sym_path in [self.symbols_file] + gamerom_paths:
if sym_path and os.path.isfile(sym_path):
logger.debug("Loading symbol file: %s", sym_path)
if sym_path.endswith(".map"):
self._load_map_file(sym_path)
else:
self._load_sym_file(sym_path)
return self.rom_symbols
def _load_sym_file(self, sym_path):
group = "labels"
with open(sym_path) as f:
for _line in f.readlines():
line = _line.strip()
if line == "":
continue
elif line.startswith(";"):
continue
elif line.startswith("["):
# Start of key group
group = line.strip()[1:-1]
# [labels]
# [definitions]
continue
if group == "labels":
try:
bank, addr, sym_label = re.split(":| ", line.strip())
bank = int(bank, 16)
addr = int(addr, 16)
if bank not in self.rom_symbols:
self.rom_symbols[bank] = {}
if addr not in self.rom_symbols[bank]:
self.rom_symbols[bank][addr] = []
self.rom_symbols[bank][addr].append(sym_label)
self.rom_symbols_inverse[sym_label] = (bank, addr)
except ValueError:
logger.debug("Skipping .sym line: %s", line.strip())
elif group == "definitions":
pass
else:
logger.warning("Invalid group. Skipping .sym line: %s", line.strip())
def _load_map_file(self, map_path):
# .map files have no bank info, everything goes in bank 0
bank = 0
with open(map_path) as f:
for line in f:
match = self._MAP_SYMBOL_RE.match(line)
if match is None:
continue
addr_str, sym_label = match.groups()
addr = int(addr_str, 16)
if sym_label in self.rom_symbols_inverse:
continue
if bank not in self.rom_symbols:
self.rom_symbols[bank] = {}
if addr not in self.rom_symbols[bank]:
self.rom_symbols[bank][addr] = []
self.rom_symbols[bank][addr].append(sym_label)
self.rom_symbols_inverse[sym_label] = (bank, addr)
def _lookup_symbol(self, symbol):
bank_addr = self.rom_symbols_inverse.get(symbol)
if bank_addr is None:
raise ValueError("Symbol not found: %s" % symbol)
return bank_addr
[docs]
def symbol_lookup(self, symbol):
"""
Look up a symbol from the loaded `.sym` or `.map` file.
This can be useful in combination with `PyBoy.memory` or even `PyBoy.hook_register`.
See `PyBoy.hook_register` for how to load symbol into PyBoy.
Example:
```python
>>> # Directly
>>> pyboy.memory[pyboy.symbol_lookup("Tileset")]
0
>>> # By bank and address
>>> bank, addr = pyboy.symbol_lookup("Tileset")
>>> pyboy.memory[bank, addr]
0
>>> pyboy.memory[bank, addr:addr+10]
[0, 0, 0, 0, 0, 0, 102, 102, 102, 102]
```
Args:
symbol (str): Symbol name to look up.
Returns:
tuple[int, int]: ROM/RAM bank and address.
Raises:
ValueError: If the symbol is not found in the loaded symbol files.
"""
return self._lookup_symbol(symbol)
[docs]
def hook_register(self, bank, addr, callback, context):
"""
Adds a hook into a specific bank and memory address.
When the Game Boy executes this address, the provided callback function will be called.
By providing an object as `context`, you can later get access to information inside and outside of the callback.
Example:
```python
>>> context = "Hello from hook"
>>> def my_callback(context):
... print(context)
>>> pyboy.hook_register(0, 0x100, my_callback, context)
>>> pyboy.tick(70)
Hello from hook
True
```
If a symbol file is loaded, this function can also automatically resolve a bank and address from a symbol. To
enable this, you'll need to place a `.sym` file next to your ROM, or provide it using:
`PyBoy(..., symbols="game_rom.gb.sym")`.
Then provide `None` for `bank` and the symbol for `addr` to trigger the automatic lookup.
Example:
```python
>>> # Continued example above
>>> pyboy.hook_register(None, "Main.move", lambda x: print(x), "Hello from hook2")
>>> pyboy.tick(81)
Hello from hook2
True
```
**NOTE**:
Don't register hooks to something that isn't executable (graphics data etc.). This will cause your game to show
weird behavior or crash. Hooks are installed by replacing the instruction at the bank and address with a special
opcode (`0xDB`). If the address is read by the game instead of executed as code, this value will be read instead.
Args:
bank (int or None): ROM or RAM bank (None for symbol lookup)
addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
callback (func): A function which takes `context` as argument
context (object): Argument to pass to callback when hook is called
"""
if bank is None and isinstance(addr, str):
bank, addr = self._lookup_symbol(addr)
opcode = self.memory[bank, addr]
if opcode == OPCODE_BRK:
raise ValueError("Hook already registered for this bank and address.")
self.mb.breakpoint_add(bank, addr)
bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
logger.debug("Adding hook for opcode %08x", bank_addr_opcode)
self._hooks[bank_addr_opcode] = (callback, context)
[docs]
def hook_deregister(self, bank, addr):
"""
Remove a previously registered hook from a specific bank and memory address.
Example:
```python
>>> context = "Hello from hook"
>>> def my_callback(context):
... print(context)
>>> pyboy.hook_register(0, 0x2000, my_callback, context)
>>> pyboy.hook_deregister(0, 0x2000)
```
This function can also deregister a hook based on a symbol. See `PyBoy.hook_register` for details.
Example:
```python
>>> pyboy.hook_register(None, "Main", lambda x: print(x), "Hello from hook")
>>> pyboy.hook_deregister(None, "Main")
```
Args:
bank (int or None): ROM or RAM bank (None for symbol lookup)
addr (int or str): Address in the Game Boy's address space (str for symbol lookup)
"""
if bank is None and isinstance(addr, str):
bank, addr = self._lookup_symbol(addr)
breakpoint_meta = self.mb.breakpoint_find(bank, addr)
if not breakpoint_meta:
raise ValueError("Breakpoint not found for bank and addr")
_, _, opcode = breakpoint_meta
self.mb.breakpoint_remove(bank, addr)
bank_addr_opcode = (bank & 0xFF) << 24 | (addr & 0xFFFF) << 8 | (opcode & 0xFF)
self._hooks.pop(bank_addr_opcode)
def _handle_hooks(self):
if _handler := self._hooks.get(self.mb.breakpoint_waiting):
(callback, context) = _handler
callback(context)
return True
return False
[docs]
def get_sprite(self, sprite_index):
"""
Provides a `pyboy.api.sprite.Sprite` object, which makes the OAM data more presentable. The given index
corresponds to index of the sprite in the "Object Attribute Memory" (OAM).
The Game Boy supports 40 sprites in total. Read more in the [Pan Docs: OAM](https://gbdev.io/pandocs/OAM.html).
```python
>>> s = pyboy.get_sprite(12)
>>> s
Sprite [12]: Position: (-8, -16), Shape: (8, 8), Tiles: (Tile: 0), On screen: False
>>> s.on_screen
False
>>> s.tiles
[Tile: 0]
```
Args:
sprite_index (int): Sprite index from 0 to 39.
Returns:
`pyboy.api.sprite.Sprite`: Sprite corresponding to the given index.
Raises:
PyBoyOutOfBoundsException: If `sprite_index` is outside 0 to 39.
"""
return Sprite(self.mb, sprite_index)
[docs]
def get_sprite_by_tile_identifier(self, tile_identifiers, on_screen=True):
"""
Provided a list of tile identifiers, this function will find all occurrences of sprites using the tile
identifiers and return the sprite indexes where each identifier is found. Use the sprite indexes in the
`pyboy.PyBoy.get_sprite` function to get a `pyboy.api.sprite.Sprite` object.
Example:
```python
>>> print(pyboy.get_sprite_by_tile_identifier([43, 123]))
[[0, 2, 4], []]
```
Meaning, that tile identifier `43` is found at the sprite indexes: 0, 2, and 4, while tile identifier
`123` was not found anywhere.
Args:
tile_identifiers (list[int]): Tile identifiers to search for.
on_screen (bool): Require that the matched sprite is on screen
Returns
-------
list[list[int]]:
Sprite indices grouped by the corresponding input tile identifier.
"""
matches = []
for i in tile_identifiers:
match = []
for s in range(constants.SPRITES):
sprite = Sprite(self.mb, s)
for t in sprite.tiles:
if t.tile_identifier == i and (not on_screen or (on_screen and sprite.on_screen)):
match.append(s)
matches.append(match)
return matches
[docs]
def get_tile(self, identifier):
"""
The Game Boy can have 384 tiles loaded in memory at once (768 for Game Boy Color). Use this method to get a
`pyboy.api.tile.Tile`-object for given identifier.
The identifier is a PyBoy construct, which unifies two different scopes of indexes in the Game Boy hardware. See
the `pyboy.api.tile.Tile` object for more information.
Example:
```python
>>> t = pyboy.get_tile(2)
>>> t
Tile: 2
>>> t.shape
(8, 8)
```
Args:
identifier (int): Tile identifier from 0 to 383 on DMG or 0 to 767 on CGB.
Returns:
`pyboy.api.tile.Tile`: A Tile object for the given identifier.
Raises:
PyBoyOutOfBoundsException: If `identifier` is outside the available tile range.
"""
return Tile(self.mb, identifier=identifier)
[docs]
def rtc_lock_experimental(self, enable):
"""
**WARN: This is an experimental API and is subject to change.**
Lock the Real Time Clock (RTC) of a supporting cartridge. It might be advantageous to lock the RTC when training
an AI in games that use it to change behavior (i.e. day and night).
The first time the game is turned on, an `.rtc` file is created with the current time. This is the epoch for the
RTC. When using `rtc_lock_experimental`, the RTC will always report this point in time. If you let the game
progress first, before using `rtc_lock_experimental`, the internal clock will move backwards and might corrupt
the game.
Example:
```python
>>> pyboy = PyBoy('game_rom.gb')
>>> pyboy.rtc_lock_experimental(True) # RTC will not progress
```
**WARN: This is an experimental API and is subject to change.**
Args:
enable (bool): True to lock RTC, False to operate normally
"""
if self.mb.cartridge.rtc_enabled:
self.mb.cartridge.rtc.timelock = enable
else:
raise PyBoyException("There's no RTC for this cartridge type")
def _cycles(self):
return self.mb.cpu.cycles
[docs]
class PyBoyRegisterFile:
"""
This class cannot be used directly, but is accessed through `PyBoy.register_file`.
This class serves the purpose of reading and writing to the CPU registers. It's best used inside the callback
registered with `PyBoy.hook_register`, as `PyBoy.tick` doesn't return at a specific point.
See the [Pan Docs: CPU registers and flags](https://gbdev.io/pandocs/CPU_Registers_and_Flags.html) for a great overview.
Registers are accessed with the following names: `A, F, B, C, D, E, HL, SP, PC` where the last three are 16-bit and
the others are 8-bit. Reads return integers. Assignments accept integers and mask them to the register width; `F`
keeps only its upper four flag bits.
Example:
```python
>>> def my_callback(pyboy):
... print("Register A:", pyboy.register_file.A)
... pyboy.memory[0xFF50] = 1 # Example: Disable boot ROM
... pyboy.register_file.A = 0x11 # Modify to the needed value
... pyboy.register_file.PC = 0x100 # Jump past existing code
>>> pyboy.hook_register(-1, 0xFC, my_callback, pyboy)
>>> pyboy.tick(120)
Register A: 1
True
```
"""
def __init__(self, cpu):
self.cpu = cpu
@property
def A(self):
return self.cpu.A
@A.setter
def A(self, value):
self.cpu.A = value & 0xFF
@property
def F(self):
return self.cpu.F
@F.setter
def F(self, value):
self.cpu.F = value & 0xF0
@property
def B(self):
return self.cpu.B
@B.setter
def B(self, value):
self.cpu.B = value & 0xFF
@property
def C(self):
return self.cpu.C
@C.setter
def C(self, value):
self.cpu.C = value & 0xFF
@property
def D(self):
return self.cpu.D
@D.setter
def D(self, value):
self.cpu.D = value & 0xFF
@property
def E(self):
return self.cpu.E
@E.setter
def E(self, value):
self.cpu.E = value & 0xFF
@property
def HL(self):
return self.cpu.HL
@HL.setter
def HL(self, value):
self.cpu.HL = value & 0xFFFF
@property
def SP(self):
return self.cpu.SP
@SP.setter
def SP(self, value):
self.cpu.SP = value & 0xFFFF
@property
def PC(self):
return self.cpu.PC
@PC.setter
def PC(self, value):
self.cpu.PC = value & 0xFFFF
[docs]
class PyBoyMemoryView:
"""
This class cannot be used directly, but is accessed through `PyBoy.memory`.
This class serves four purposes: Reading memory (ROM/RAM), writing memory (RAM), overriding memory (ROM) and special registers.
See the [Pan Docs: Memory Map](https://gbdev.io/pandocs/Memory_Map.html) for a great overview of the memory space.
Memory can be accessed as individual bytes (`pyboy.memory[0x00]`) or as slices (`pyboy.memory[0x00:0x10]`). And if
applicable, a specific ROM/RAM bank can be defined before the address (`pyboy.memory[0, 0x00]` or `pyboy.memory[0, 0x00:0x10]`).
The boot ROM is accessed using the special `-1` ROM bank.
The find addresses of interest, either search online for something like: "[game title] RAM map", or find them yourself
using `PyBoy.memory_scanner`.
**Read:**
If you're developing a bot or AI with this API, you're most likely going to be using read the most. This is how you
would efficiently read the score, time, coins, positions etc. in a game's memory.
At this point, all reads will return a new list of the values in the given range. The slices will not reference back to the PyBoy memory. This feature might come in the future.
```python
>>> pyboy.memory[0x0000] # Read one byte at address 0x0000
49
>>> pyboy.memory[0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010)
[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
>>> pyboy.memory[-1, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from the boot ROM
[49, 254, 255, 33, 0, 128, 175, 34, 124, 254, 160, 32, 249, 6, 48, 33]
>>> pyboy.memory[0, 0x0000:0x0010] # Read 16 bytes from 0x0000 to 0x0010 (excluding 0x0010) from ROM bank 0
[64, 65, 66, 67, 68, 69, 70, 65, 65, 65, 71, 65, 65, 65, 72, 73]
>>> pyboy.memory[2, 0xA000] # Read from external RAM on cartridge (if any) from bank 2 at address 0xA000
0
```
**Write:**
Writing to Game Boy memory can be complicated because of the limited address space. There's a lot of memory that
isn't directly accessible, and can be hidden through "memory banking". This means that the same address range
(for example 0x4000 to 0x8000) can change depending on what state the game is in.
If you want to change an address in the ROM, then look at override below. Issuing writes to the ROM area actually
sends commands to the [Memory Bank Controller (MBC)](https://gbdev.io/pandocs/MBCs.html#mbcs) on the cartridge.
A write is done by assigning to the `PyBoy.memory` object. It's recommended to define the bank to avoid mistakes
(`pyboy.memory[2, 0xA000]=1`). Without defining the bank, PyBoy will pick the current bank for the given address if
needed (`pyboy.memory[0xA000]=1`).
```python
>>> pyboy.memory[0xC000] = 123 # Write to WRAM at address 0xC000
>>> pyboy.memory[0xC000:0xC00A] = [0,1,2,3,4,5,6,7,8,9] # Write to WRAM from address 0xC000 to 0xC00A
>>> pyboy.memory[0xC010:0xC01A] = 0 # Write to WRAM from address 0xC010 to 0xC01A
>>> pyboy.memory[0x1000] = 123 # Not writing 123 at address 0x1000! This sends a command to the cartridge's MBC.
>>> pyboy.memory[2, 0xA000] = 123 # Write to external RAM on cartridge (if any) for bank 2 at address 0xA000
>>> # Game Boy Color (CGB) only:
>>> pyboy_cgb.memory[1, 0x8000] = 25 # Write to VRAM bank 1 at address 0x8000 when in CGB mode
>>> pyboy_cgb.memory[6, 0xD000] = 25 # Write to WRAM bank 6 at address 0xD000 when in CGB mode
```
**Override:**
Override data at a given memory address of the Game Boy's ROM.
This can be used to reprogram a game ROM to change its behavior.
This will not let you override RAM or a special register. This will let you override data in the ROM at any given bank.
This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
_NOTE_: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
any overrides when reloading the ROM.
To override, it's required to provide the ROM-bank you're changing. Otherwise, it'll be considered a regular 'write' as described above.
```python
>>> pyboy.memory[0, 0x0010] = 10 # Override ROM-bank 0 at address 0x0010
>>> pyboy.memory[0, 0x0010:0x001A] = [0,1,2,3,4,5,6,7,8,9] # Override ROM-bank 0 at address 0x0010 to 0x001A
>>> pyboy.memory[-1, 0x0010] = 10 # Override boot ROM at address 0x0010
>>> pyboy.memory[1, 0x6000] = 12 # Override ROM-bank 1 at address 0x6000
>>> pyboy.memory[0x1000] = 12 # This will not override, as there is no ROM bank assigned!
```
**Special Registers:**
The Game Boy has a range of memory addresses known as [hardware registers](https://gbdev.io/pandocs/Hardware_Reg_List.html). These control parts of the hardware like LCD,
Timer, DMA, serial and so on. Even though they might appear as regular RAM addresses, reading/writing these addresses
often results in special side-effects.
The [DIV (0xFF04) register](https://gbdev.io/pandocs/Timer_and_Divider_Registers.html#ff04--div-divider-register) for example provides a number that increments 16 thousand times each second. This can be
used as a source of randomness in games. If you read the value, you'll get a pseudo-random number. But if you write
*any* value to the register, it'll reset to zero.
```python
>>> pyboy.memory[0xFF04] # DIV register
231
>>> pyboy.memory[0xFF04] = 123 # Trying to write to it will always reset it to zero
>>> pyboy.memory[0xFF04]
0
```
"""
def __init__(self, mb):
self.mb = mb
def _fix_slice(self, addr):
if addr.start is None:
return (-1, 0, 0)
if addr.stop is None:
return (0, -1, 0)
start = addr.start
stop = addr.stop
if start > stop:
return (-1, -1, 0)
try:
start = index(start)
stop = index(stop)
step = 1 if addr.step is None else index(addr.step)
except TypeError:
raise PyBoyInvalidInputException("Slice bounds and step must be integers") from None
if step <= 0:
raise PyBoyInvalidInputException("Slice step must be a positive integer")
return start, stop, step
def __len__(self):
raise PyBoyInvalidOperationException(
"It's not possible to define the length of the memory space. See instead https://gbdev.io/pandocs/Memory_Map.html"
)
def __iter__(self):
"""
Address space is overlapping, and therefore too complex to return as list or iterator.
If you want a snapshot, you should request specific memory ranges and banks.
See https://gbdev.io/pandocs/Memory_Map.html
"""
raise PyBoyInvalidOperationException("It's not possible to iterate over the memory space.")
def __getitem__(self, addr):
is_bank = isinstance(addr, tuple)
bank = 0
if is_bank:
bank, addr = addr
if not (isinstance(bank, int)):
raise PyBoyInvalidInputException("Bank has to be integer. Slicing is not supported.")
is_single = isinstance(addr, int)
if not is_single:
start, stop, step = self._fix_slice(addr)
if not (start >= 0):
raise PyBoyInvalidInputException("Start address required")
if not (stop >= 0):
raise PyBoyInvalidInputException("End address required")
if not 0 <= start <= 0xFFFF:
raise PyBoyOutOfBoundsException("Start address out of bounds")
if not 0 <= stop <= 0x10000:
raise PyBoyOutOfBoundsException("End address out of bounds")
if not (start < stop):
raise PyBoyInvalidInputException("Start address has to come before end address")
return self.__getitem(start, stop, step, bank, is_single, is_bank)
else:
return self.__getitem(addr, 0, 1, bank, is_single, is_bank)
def __getitem(self, start, stop, step, bank, is_single, is_bank):
slice_length = (stop - start) // step
if is_bank:
# Reading a specific bank
if start < 0x8000:
if start >= 0x4000:
start -= 0x4000
stop -= 0x4000
# Cartridge ROM Banks
if not (stop < 0x4000):
raise PyBoyOutOfBoundsException("Out of bounds for reading ROM bank")
if bank == -1:
if not (start <= 0xFF):
raise PyBoyOutOfBoundsException("Start address out of range for bootrom")
if not (stop <= 0xFF):
raise PyBoyOutOfBoundsException("Start address out of range for bootrom")
if not is_single:
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.bootrom.bootrom[x]
return mem_slice
else:
return self.mb.bootrom.bootrom[start]
else:
if not (bank <= self.mb.cartridge.external_rom_count):
raise PyBoyOutOfBoundsException("ROM Bank out of range")
if not is_single:
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.cartridge.rombanks[bank, x]
return mem_slice
else:
return self.mb.cartridge.rombanks[bank, start]
elif start < 0xA000:
start -= 0x8000
stop -= 0x8000
# CGB VRAM Banks
if not (self.mb.cgb or (bank == 0)):
raise PyBoyInvalidInputException("Selecting bank of VRAM is only supported for CGB mode")
if not (stop < 0x2000):
raise PyBoyOutOfBoundsException("Out of bounds for reading VRAM bank")
if not (bank <= 1):
raise PyBoyOutOfBoundsException("VRAM Bank out of range")
if bank == 0:
if not is_single:
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.lcd.VRAM0[x]
return mem_slice
else:
return self.mb.lcd.VRAM0[start]
else:
if not is_single:
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.lcd.VRAM1[x]
return mem_slice
else:
return self.mb.lcd.VRAM1[start]
elif start < 0xC000:
start -= 0xA000
stop -= 0xA000
# Cartridge RAM banks
if not (stop < 0x2000):
raise PyBoyOutOfBoundsException("Out of bounds for reading cartridge RAM bank")
if not (bank <= self.mb.cartridge.external_ram_count):
raise PyBoyOutOfBoundsException("ROM Bank out of range")
if not is_single:
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.cartridge.rambanks[bank, x]
return mem_slice
else:
return self.mb.cartridge.rambanks[bank, start]
elif start < 0xE000:
start -= 0xC000
stop -= 0xC000
if start >= 0x1000:
start -= 0x1000
stop -= 0x1000
# CGB VRAM banks
if not (self.mb.cgb or (bank == 0)):
raise PyBoyInvalidInputException("Selecting bank of WRAM is only supported for CGB mode")
if not (stop < 0x1000):
raise PyBoyOutOfBoundsException("Out of bounds for reading VRAM bank")
if not (bank <= 7):
raise PyBoyOutOfBoundsException("WRAM Bank out of range")
if not is_single:
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.ram.internal_ram0[x + bank * 0x1000]
return mem_slice
else:
return self.mb.ram.internal_ram0[start + bank * 0x1000]
else:
raise PyBoyInvalidInputException("Invalid memory address for bank")
elif not is_single:
# Reading slice of memory space
mem_slice = [0] * slice_length
for x in range(start, stop, step):
mem_slice[(x - start) // step] = self.mb.getitem(x)
return mem_slice
else:
# Reading specific address of memory space
return self.mb.getitem(start)
def __setitem__(self, addr, v):
is_bank = isinstance(addr, tuple)
bank = 0
if is_bank:
bank, addr = addr
if not (isinstance(bank, int)):
raise PyBoyInvalidInputException("Bank has to be integer. Slicing is not supported.")
is_single = isinstance(addr, int)
if not is_single:
start, stop, step = self._fix_slice(addr)
if not (start >= 0):
raise PyBoyInvalidInputException("Start address required")
if not (stop >= 0):
raise PyBoyInvalidInputException("End address required")
if not 0 <= start <= 0xFFFF:
raise PyBoyOutOfBoundsException("Start address out of bounds")
if not 0 <= stop <= 0x10000:
raise PyBoyOutOfBoundsException("End address out of bounds")
if not (start < stop):
raise PyBoyInvalidInputException("Start address has to come before end address")
self.__setitem(start, stop, step, v, bank, is_single, is_bank)
else:
self.__setitem(addr, 0, 0, v, bank, is_single, is_bank)
def __setitem(self, start, stop, step, v, bank, is_single, is_bank):
if is_bank:
# Writing a specific bank
if start < 0x8000:
"""
Override one byte at a given memory address of the Game Boy's ROM.
This will let you override data in the ROM at any given bank. This is the memory allocated at 0x0000 to 0x8000, where 0x4000 to 0x8000 can be changed from the MBC.
__NOTE__: Any changes here are not saved or loaded to game states! Use this function with caution and reapply
any overrides when reloading the ROM.
If you need to change a RAM address, see `pyboy.PyBoy.memory`.
Args:
rom_bank (int): ROM bank to do the overwrite in
addr (int): Address to write the byte inside the ROM bank
value (int): A byte of data
"""
if start >= 0x4000:
start -= 0x4000
stop -= 0x4000
# Cartridge ROM Banks
if not (stop <= 0x4000):
raise PyBoyOutOfBoundsException("Out of bounds for reading ROM bank")
if not (bank <= self.mb.cartridge.external_rom_count):
raise PyBoyOutOfBoundsException("ROM Bank out of range")
if bank == -1:
if not (start <= 0xFF):
raise PyBoyOutOfBoundsException("Start address out of range for bootrom")
if not (stop <= 0x100):
raise PyBoyOutOfBoundsException("Start address out of range for bootrom")
if not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.bootrom.bootrom[x] = next(_v)
else:
for x in range(start, stop, step):
self.mb.bootrom.bootrom[x] = v
else:
self.mb.bootrom.bootrom[start] = v
else:
if not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.cartridge.overrideitem(bank, x, next(_v))
else:
for x in range(start, stop, step):
self.mb.cartridge.overrideitem(bank, x, v)
else:
self.mb.cartridge.overrideitem(bank, start, v)
elif start < 0xA000:
start -= 0x8000
stop -= 0x8000
# CGB VRAM Banks
if not (self.mb.cgb or (bank == 0)):
raise PyBoyInvalidInputException("Selecting bank of VRAM is only supported for CGB mode")
if not (stop <= 0x2000):
raise PyBoyOutOfBoundsException("Out of bounds for reading VRAM bank")
if not (bank <= 1):
raise PyBoyOutOfBoundsException("VRAM Bank out of range")
if bank == 0:
if not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.lcd.VRAM0[x] = next(_v)
else:
for x in range(start, stop, step):
self.mb.lcd.VRAM0[x] = v
else:
self.mb.lcd.VRAM0[start] = v
else:
if not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.lcd.VRAM1[x] = next(_v)
else:
for x in range(start, stop, step):
self.mb.lcd.VRAM1[x] = v
else:
self.mb.lcd.VRAM1[start] = v
elif start < 0xC000:
start -= 0xA000
stop -= 0xA000
# Cartridge RAM banks
if not (stop <= 0x2000):
raise PyBoyOutOfBoundsException("Out of bounds for reading cartridge RAM bank")
if not (bank <= self.mb.cartridge.external_ram_count):
raise PyBoyOutOfBoundsException("ROM Bank out of range")
if not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.cartridge.rambanks[bank, x] = next(_v)
else:
for x in range(start, stop, step):
self.mb.cartridge.rambanks[bank, x] = v
else:
self.mb.cartridge.rambanks[bank, start] = v
elif start < 0xE000:
start -= 0xC000
stop -= 0xC000
if start >= 0x1000:
start -= 0x1000
stop -= 0x1000
# CGB VRAM banks
if not (self.mb.cgb or (bank == 0)):
raise PyBoyInvalidInputException("Selecting bank of WRAM is only supported for CGB mode")
if not (stop <= 0x1000):
raise PyBoyOutOfBoundsException("Out of bounds for reading VRAM bank")
if not (bank <= 7):
raise PyBoyOutOfBoundsException("WRAM Bank out of range")
if not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.ram.internal_ram0[x + bank * 0x1000] = next(_v)
else:
for x in range(start, stop, step):
self.mb.ram.internal_ram0[x + bank * 0x1000] = v
else:
self.mb.ram.internal_ram0[start + bank * 0x1000] = v
else:
raise PyBoyInvalidInputException("Invalid memory address for bank")
elif not is_single:
# Writing slice of memory space
if hasattr(v, "__iter__"):
if not ((stop - start) // step == len(v)):
raise PyBoyInvalidInputException("slice does not match length of data")
_v = iter(v)
for x in range(start, stop, step):
self.mb.setitem(x, next(_v))
else:
for x in range(start, stop, step):
self.mb.setitem(x, v)
else:
# Writing specific address of memory space
self.mb.setitem(start, v)