Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically loading plugins #233

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
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