Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pyboy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@

from .pyboy import PyBoy
from .utils import WindowEvent


def get_include():
import os
return os.path.dirname(os.path.abspath(__file__))
8 changes: 5 additions & 3 deletions pyboy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from pyboy import PyBoy, core
from pyboy.logger import log_level, logger
from pyboy.plugins.manager import parser_arguments
from pyboy.plugin_manager import external_plugin_names, parser_arguments, window_names
from pyboy.pyboy import defaults

INTERNAL_LOADSTATE = "INTERNAL_LOADSTATE_TOKEN"
Expand All @@ -29,8 +29,10 @@ def valid_file_path(path):


parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter, # Don't wrap epilog automatically
description="PyBoy -- Game Boy emulator written in Python",
epilog="Warning: Features marked with (internal use) might be subject to change.",
epilog=(f"External plugins loaded: {external_plugin_names()}\n\n" if external_plugin_names() else "") +
"Warning: Features marked with (internal use) might be subject to change.",
)
parser.add_argument("ROM", type=valid_file_path, help="Path to a Game Boy compatible ROM file")
parser.add_argument("-b", "--bootrom", type=valid_file_path, help="Path to a boot-ROM file")
Expand Down Expand Up @@ -67,7 +69,7 @@ def valid_file_path(path):
"--window",
default=defaults["window_type"],
type=str,
choices=["SDL2", "OpenGL", "headless", "dummy"],
choices=list(window_names()),
help="Specify window-type to use"
)
parser.add_argument("-s", "--scale", default=defaults["scale"], type=int, help="The scaling multiplier for the window")
Expand Down
30 changes: 30 additions & 0 deletions pyboy/plugin_manager.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#

cimport cython
from pyboy.plugins.base_plugin cimport PyBoyPlugin, PyBoyWindowPlugin



cdef class PluginManager:
cdef object pyboy

cdef list enabled_plugins
cdef list enabled_window_plugins
cdef list enabled_debug_plugins
cdef list enabled_gamewrappers

cdef dict plugin_mapping
cpdef list list_plugins(self)
cpdef PyBoyPlugin get_plugin(self, str)

cdef list handle_events(self, list)
cdef void post_tick(self)
cdef void _post_tick_windows(self)
cdef void frame_limiter(self, int)
cdef str window_title(self)
cdef void stop(self)
cdef void _set_title(self)
cdef void handle_breakpoint(self)
153 changes: 153 additions & 0 deletions pyboy/plugin_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#

import importlib
import inspect
import logging
import os
import sys
import sysconfig
from pathlib import Path
from pkgutil import iter_modules

from pyboy import plugins
from pyboy.plugins.base_plugin import PyBoyDebugPlugin, PyBoyGameWrapper, PyBoyPlugin, PyBoyWindowPlugin

if sys.version_info >= (3, 8):
from importlib import metadata as importlib_metadata
else:
import importlib_metadata

logger = logging.getLogger(__name__)

EXT_SUFFIX = sysconfig.get_config_var("EXT_SUFFIX")

registered_plugins = []
registered_window_plugins = []
registered_gamewrappers = []

enabled_plugins = []
enabled_window_plugins = []
enabled_gamewrappers = []

builtin_plugins = [importlib.import_module("pyboy.plugins." + m.name) for m in iter_modules(plugins.__path__)]
external_plugins = []
Comment on lines +35 to +36
Copy link
Owner Author

@Baekalfen Baekalfen Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want something like Django's list of middlewares to manage loading/execution order.

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
]

for p in importlib_metadata.distributions():
for e in p.entry_points:
if e.group == "pyboy":
external_plugins.append(e.load())

for mod in builtin_plugins + external_plugins:
if hasattr(mod, "_export_plugins"):
plugin_names = getattr(mod, "_export_plugins")
else:
plugin_names = [x for x in dir(mod) if not x.startswith("_")]

for attr_name in plugin_names:
_mod_class = getattr(mod, attr_name)
if inspect.isclass(_mod_class) and issubclass(_mod_class, PyBoyPlugin) and _mod_class not in [
PyBoyPlugin, PyBoyWindowPlugin, PyBoyGameWrapper, PyBoyDebugPlugin
]:
if issubclass(_mod_class, PyBoyGameWrapper):
registered_gamewrappers.append(_mod_class)
elif issubclass(_mod_class, PyBoyWindowPlugin):
registered_window_plugins.append(_mod_class)
else:
registered_plugins.append(_mod_class)


def parser_arguments():
for p in registered_plugins + registered_window_plugins + registered_gamewrappers:
yield p.argv


def window_names():
for p in registered_window_plugins:
if p.name:
yield p.name


def external_plugin_names():
return ", ".join([p.__name__ for p in external_plugins])


class PluginManager:
def __init__(self, pyboy, mb, pyboy_argv):
self.pyboy = pyboy

if external_plugins:
logger.info(f"External plugins loaded: {external_plugin_names()}")
else:
logger.info("No external plugins found")

self.enabled_plugins = [p(pyboy, mb, pyboy_argv) for p in registered_plugins if p.enabled(pyboy, pyboy_argv)]
self.enabled_window_plugins = [
p(pyboy, mb, pyboy_argv) for p in registered_window_plugins if p.enabled(pyboy, pyboy_argv)
]
self.enabled_debug_plugins = [p for p in self.enabled_window_plugins if isinstance(p, PyBoyDebugPlugin)]
self.enabled_gamewrappers = [
p(pyboy, mb, pyboy_argv) for p in registered_gamewrappers if p.enabled(pyboy, pyboy_argv)
]

self.plugin_mapping = {}
for p in self.enabled_window_plugins + self.enabled_plugins + self.enabled_gamewrappers:
self.plugin_mapping[p.__class__.__name__] = p

def list_plugins(self):
return list(self.plugin_mapping.keys())

def get_plugin(self, name):
return self.plugin_mapping[name]

def gamewrapper(self):
if self.enabled_gamewrappers:
# There should be exactly one enabled, if any.
return self.enabled_gamewrappers[0]
return None

def handle_events(self, events):
for p in self.enabled_window_plugins + self.enabled_plugins + self.enabled_gamewrappers:
events = p.handle_events(events)
return events

def post_tick(self):
for p in self.enabled_plugins + self.enabled_gamewrappers:
p.post_tick()
self._post_tick_windows()
self._set_title()

def _set_title(self):
for p in self.enabled_window_plugins:
p.set_title(self.pyboy.window_title)
pass

def _post_tick_windows(self):
for p in self.enabled_window_plugins:
p.post_tick()
pass

def frame_limiter(self, speed):
if speed <= 0:
return

for p in self.enabled_window_plugins:
if p.frame_limiter(speed):
return

def window_title(self):
title = ""
for p in self.enabled_window_plugins + self.enabled_plugins + self.enabled_gamewrappers:
title += p.window_title()
return title

def stop(self):
for p in self.enabled_window_plugins + self.enabled_plugins + self.enabled_gamewrappers:
p.stop()
pass

def handle_breakpoint(self):
for p in self.enabled_debug_plugins:
p.handle_breakpoint()
pass
5 changes: 3 additions & 2 deletions pyboy/plugins/auto_pause.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ def handle_events(self, events):
events.append(WindowEvent.UNPAUSE)
return events

def enabled(self):
return self.pyboy_argv.get("autopause")
@classmethod
def enabled(cls, pyboy, pyboy_argv):
return pyboy_argv.get("autopause")
17 changes: 9 additions & 8 deletions pyboy/plugins/base_plugin.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ cdef class PyBoyPlugin:
cdef bint cgb
cdef dict pyboy_argv
@cython.locals(event=WindowEvent)
cdef list handle_events(self, list)
cdef void post_tick(self)
cdef str window_title(self)
cdef void stop(self)
cpdef bint enabled(self)
cpdef list handle_events(self, list)
cpdef void post_tick(self)
cpdef str window_title(self)
cpdef void stop(self)

# cpdef bint enabled(cls, object, dict)


cdef class PyBoyWindowPlugin(PyBoyPlugin):
Expand All @@ -34,8 +35,8 @@ cdef class PyBoyWindowPlugin(PyBoyPlugin):
cdef bint enable_title
cdef Renderer renderer

cdef bint frame_limiter(self, int)
cdef void set_title(self, str)
cpdef bint frame_limiter(self, int)
cpdef void set_title(self, str)


cdef class PyBoyGameWrapper(PyBoyPlugin):
Expand All @@ -47,7 +48,7 @@ cdef class PyBoyGameWrapper(PyBoyPlugin):
cdef array _cached_game_area_tiles_raw
cdef uint32_t[:, :] _cached_game_area_tiles
@cython.locals(xx=int, yy=int, width=int, height=int, SCX=int, SCY=int, _x=int, _y=int)
cdef uint32_t[:, :] _game_area_tiles(self)
cpdef uint32_t[:, :] _game_area_tiles(self)

cdef bint game_area_wrap_around
cdef tuple game_area_section
Expand Down
18 changes: 12 additions & 6 deletions pyboy/plugins/base_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ def window_title(self):
def stop(self):
pass

def enabled(self):
@classmethod
def enabled(cls, pyboy, pyboy_argv):
return True


class PyBoyWindowPlugin(PyBoyPlugin):
name = "PyBoyWindowPlugin"

def __init__(self, pyboy, mb, pyboy_argv, *args, **kwargs):
super().__init__(pyboy, mb, pyboy_argv, *args, **kwargs)

if not self.enabled():
return

scale = pyboy_argv.get("scale")
self.scale = scale
logger.info("%s initialization" % self.__class__.__name__)
Expand All @@ -88,6 +88,11 @@ def set_title(self, title):
pass


class PyBoyDebugPlugin(PyBoyWindowPlugin):
def handle_breakpoint(self):
pass


class PyBoyGameWrapper(PyBoyPlugin):
"""
This is the base-class for the game-wrappers. It provides some generic game-wrapping functionality, like `game_area`
Expand Down Expand Up @@ -117,8 +122,9 @@ def __init__(self, *args, game_area_section=(0, 0, 32, 32), game_area_wrap_aroun
v = memoryview(self._cached_game_area_tiles_raw).cast("I")
self._cached_game_area_tiles = [v[i:i + height] for i in range(0, height * width, height)]

def enabled(self):
return self.pyboy_argv.get("game_wrapper") and self.pyboy.cartridge_title() == self.cartridge_title
@classmethod
def enabled(cls, pyboy, pyboy_argv):
return pyboy_argv.get("game_wrapper") and pyboy.cartridge_title() == cls.cartridge_title

def post_tick(self):
raise NotImplementedError("post_tick not implemented in game wrapper")
Expand Down
14 changes: 7 additions & 7 deletions pyboy/plugins/debug.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ cdef class Debug(PyBoyWindowPlugin):
cdef TileDataWindow tiledata1
cdef MemoryWindow memory
cdef bint sdl2_event_pump
cdef void handle_breakpoint(self)
cpdef void handle_breakpoint(self)


cdef class BaseDebugWindow(PyBoyWindowPlugin):
Expand All @@ -66,7 +66,7 @@ cdef class BaseDebugWindow(PyBoyWindowPlugin):
cdef void mark_tile(self, int, int, uint32_t, int, int, bint)

@cython.locals(event=WindowEvent)
cdef list handle_events(self, list)
cpdef list handle_events(self, list)


cdef class TileViewWindow(BaseDebugWindow):
Expand All @@ -78,7 +78,7 @@ cdef class TileViewWindow(BaseDebugWindow):
cdef uint32_t[:,:] tilecache # Fixing Cython locals
cdef uint32_t[:] palette_rgb # Fixing Cython locals
@cython.locals(mem_offset=uint16_t, tile_index=int, tile_column=int, tile_row=int)
cdef void post_tick(self)
cpdef void post_tick(self)

# scanlineparameters=uint8_t[:,:],
@cython.locals(x=int, y=int, xx=int, yy=int, row=int, column=int)
Expand All @@ -91,18 +91,18 @@ cdef class TileDataWindow(BaseDebugWindow):
cdef uint32_t[:,:] tilecache # Fixing Cython locals
cdef uint32_t[:] palette_rgb # Fixing Cython locals
@cython.locals(t=int, xx=int, yy=int)
cdef void post_tick(self)
cpdef void post_tick(self)

@cython.locals(tile_x=int, tile_y=int, tile_identifier=int)
cdef list handle_events(self, list)
cpdef list handle_events(self, list)

@cython.locals(t=MarkedTile, column=int, row=int)
cdef void draw_overlay(self)


cdef class SpriteWindow(BaseDebugWindow):
@cython.locals(tile_x=int, tile_y=int, sprite_identifier=int, sprite=Sprite)
cdef list handle_events(self, list)
cpdef list handle_events(self, list)

@cython.locals(t=MarkedTile, xx=int, yy=int, sprite=Sprite, i=int)
cdef void draw_overlay(self)
Expand All @@ -115,7 +115,7 @@ cdef class SpriteWindow(BaseDebugWindow):

cdef class SpriteViewWindow(BaseDebugWindow):
@cython.locals(t=int, x=int, y=int)
cdef void post_tick(self)
cpdef void post_tick(self)

@cython.locals(t=MarkedTile, sprite=Sprite, i=int)
cdef void draw_overlay(self)
Expand Down
Loading