diff --git a/pyboy/__init__.py b/pyboy/__init__.py index 5d72d0c98..71f2ae72c 100644 --- a/pyboy/__init__.py +++ b/pyboy/__init__.py @@ -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__)) diff --git a/pyboy/__main__.py b/pyboy/__main__.py index 0345cf3a9..356f77dca 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -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" @@ -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") @@ -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") diff --git a/pyboy/plugin_manager.pxd b/pyboy/plugin_manager.pxd new file mode 100644 index 000000000..0de2dfbab --- /dev/null +++ b/pyboy/plugin_manager.pxd @@ -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) diff --git a/pyboy/plugin_manager.py b/pyboy/plugin_manager.py new file mode 100644 index 000000000..4681fc701 --- /dev/null +++ b/pyboy/plugin_manager.py @@ -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 = [] +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 diff --git a/pyboy/plugins/auto_pause.pxd b/pyboy/plugins/auto_pause.pxd deleted file mode 100644 index c2d27a79f..000000000 --- a/pyboy/plugins/auto_pause.pxd +++ /dev/null @@ -1,12 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -from pyboy.plugins.base_plugin cimport PyBoyPlugin - - -cdef class AutoPause(PyBoyPlugin): - pass - - diff --git a/pyboy/plugins/auto_pause.py b/pyboy/plugins/auto_pause.py deleted file mode 100644 index 656ef73f3..000000000 --- a/pyboy/plugins/auto_pause.py +++ /dev/null @@ -1,21 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# -from pyboy.plugins.base_plugin import PyBoyPlugin -from pyboy.utils import WindowEvent - - -class AutoPause(PyBoyPlugin): - argv = [("--autopause", {"action": "store_true", "help": "Enable auto-pausing when window looses focus"})] - - def handle_events(self, events): - for event in events: - if event == WindowEvent.WINDOW_UNFOCUS: - events.append(WindowEvent.PAUSE) - elif event == WindowEvent.WINDOW_FOCUS: - events.append(WindowEvent.UNPAUSE) - return events - - def enabled(self): - return self.pyboy_argv.get("autopause") diff --git a/pyboy/plugins/base_plugin.pxd b/pyboy/plugins/base_plugin.pxd index fdcd6eca1..cf62015b2 100644 --- a/pyboy/plugins/base_plugin.pxd +++ b/pyboy/plugins/base_plugin.pxd @@ -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): @@ -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): @@ -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 diff --git a/pyboy/plugins/base_plugin.py b/pyboy/plugins/base_plugin.py index 56da6c80f..237f148bd 100644 --- a/pyboy/plugins/base_plugin.py +++ b/pyboy/plugins/base_plugin.py @@ -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__) @@ -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` @@ -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") diff --git a/pyboy/plugins/debug.pxd b/pyboy/plugins/debug.pxd index 8ee4570c8..de7e925ab 100644 --- a/pyboy/plugins/debug.pxd +++ b/pyboy/plugins/debug.pxd @@ -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): @@ -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): @@ -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) @@ -91,10 +91,10 @@ 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) @@ -102,7 +102,7 @@ cdef class TileDataWindow(BaseDebugWindow): 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) @@ -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) diff --git a/pyboy/plugins/debug.py b/pyboy/plugins/debug.py index 30d7eebed..92cea6478 100644 --- a/pyboy/plugins/debug.py +++ b/pyboy/plugins/debug.py @@ -31,6 +31,8 @@ logger = logging.getLogger(__name__) +_export_plugins = ["Debug"] + # Mask colors: COLOR = 0x00000000 # MASK = 0x00C0C000 @@ -81,13 +83,11 @@ class Debug(PyBoyWindowPlugin): "type": str, "help": "Add breakpoints on start-up (internal use)" })] + name = None def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) - if not self.enabled(): - return - self.cgb = mb.cgb self.rom_symbols = {} @@ -256,8 +256,9 @@ def stop(self): if self.sdl2_event_pump: sdl2.SDL_Quit() - def enabled(self): - if self.pyboy_argv.get("debug"): + @classmethod + def enabled(cls, pyboy, pyboy_argv): + if pyboy_argv.get("debug"): if not sdl2: logger.error("Failed to import sdl2, needed for debug window") return False diff --git a/pyboy/plugins/disable_input.py b/pyboy/plugins/disable_input.py index 1e59cce58..25b416d55 100644 --- a/pyboy/plugins/disable_input.py +++ b/pyboy/plugins/disable_input.py @@ -11,5 +11,6 @@ class DisableInput(PyBoyPlugin): def handle_events(self, events): return [] - def enabled(self): - return self.pyboy_argv.get("no_input") + @classmethod + def enabled(cls, pyboy, pyboy_argv): + return pyboy_argv.get("no_input") diff --git a/pyboy/plugins/manager.pxd b/pyboy/plugins/manager.pxd deleted file mode 100644 index 8ad523071..000000000 --- a/pyboy/plugins/manager.pxd +++ /dev/null @@ -1,68 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -cimport cython -# imports -from pyboy.plugins.window_sdl2 cimport WindowSDL2 -from pyboy.plugins.window_open_gl cimport WindowOpenGL -from pyboy.plugins.window_headless cimport WindowHeadless -from pyboy.plugins.window_dummy cimport WindowDummy -from pyboy.plugins.debug cimport Debug -from pyboy.plugins.disable_input cimport DisableInput -from pyboy.plugins.auto_pause cimport AutoPause -from pyboy.plugins.record_replay cimport RecordReplay -from pyboy.plugins.rewind cimport Rewind -from pyboy.plugins.screen_recorder cimport ScreenRecorder -from pyboy.plugins.screenshot_recorder cimport ScreenshotRecorder -from pyboy.plugins.game_wrapper_super_mario_land cimport GameWrapperSuperMarioLand -from pyboy.plugins.game_wrapper_tetris cimport GameWrapperTetris -from pyboy.plugins.game_wrapper_kirby_dream_land cimport GameWrapperKirbyDreamLand -# imports end -from pyboy.plugins.base_plugin cimport PyBoyPlugin, PyBoyWindowPlugin - - - -cdef class PluginManager: - cdef object pyboy - - # plugin_cdef - cdef public WindowSDL2 window_sdl2 - cdef public WindowOpenGL window_open_gl - cdef public WindowHeadless window_headless - cdef public WindowDummy window_dummy - cdef public Debug debug - cdef public DisableInput disable_input - cdef public AutoPause auto_pause - cdef public RecordReplay record_replay - cdef public Rewind rewind - cdef public ScreenRecorder screen_recorder - cdef public ScreenshotRecorder screenshot_recorder - cdef public GameWrapperSuperMarioLand game_wrapper_super_mario_land - cdef public GameWrapperTetris game_wrapper_tetris - cdef public GameWrapperKirbyDreamLand game_wrapper_kirby_dream_land - cdef bint window_sdl2_enabled - cdef bint window_open_gl_enabled - cdef bint window_headless_enabled - cdef bint window_dummy_enabled - cdef bint debug_enabled - cdef bint disable_input_enabled - cdef bint auto_pause_enabled - cdef bint record_replay_enabled - cdef bint rewind_enabled - cdef bint screen_recorder_enabled - cdef bint screenshot_recorder_enabled - cdef bint game_wrapper_super_mario_land_enabled - cdef bint game_wrapper_tetris_enabled - cdef bint game_wrapper_kirby_dream_land_enabled - # plugin_cdef end - - 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) diff --git a/pyboy/plugins/manager.py b/pyboy/plugins/manager.py deleted file mode 100644 index 20ec1b8e9..000000000 --- a/pyboy/plugins/manager.py +++ /dev/null @@ -1,279 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# - -# imports -from pyboy.plugins.window_sdl2 import WindowSDL2 # isort:skip -from pyboy.plugins.window_open_gl import WindowOpenGL # isort:skip -from pyboy.plugins.window_headless import WindowHeadless # isort:skip -from pyboy.plugins.window_dummy import WindowDummy # isort:skip -from pyboy.plugins.debug import Debug # isort:skip -from pyboy.plugins.disable_input import DisableInput # isort:skip -from pyboy.plugins.auto_pause import AutoPause # isort:skip -from pyboy.plugins.record_replay import RecordReplay # isort:skip -from pyboy.plugins.rewind import Rewind # isort:skip -from pyboy.plugins.screen_recorder import ScreenRecorder # isort:skip -from pyboy.plugins.screenshot_recorder import ScreenshotRecorder # isort:skip -from pyboy.plugins.game_wrapper_super_mario_land import GameWrapperSuperMarioLand # isort:skip -from pyboy.plugins.game_wrapper_tetris import GameWrapperTetris # isort:skip -from pyboy.plugins.game_wrapper_kirby_dream_land import GameWrapperKirbyDreamLand # isort:skip -# imports end - - -def parser_arguments(): - # yield_plugins - yield WindowSDL2.argv - yield WindowOpenGL.argv - yield WindowHeadless.argv - yield WindowDummy.argv - yield Debug.argv - yield DisableInput.argv - yield AutoPause.argv - yield RecordReplay.argv - yield Rewind.argv - yield ScreenRecorder.argv - yield ScreenshotRecorder.argv - yield GameWrapperSuperMarioLand.argv - yield GameWrapperTetris.argv - yield GameWrapperKirbyDreamLand.argv - # yield_plugins end - pass - - -class PluginManager: - def __init__(self, pyboy, mb, pyboy_argv): - self.pyboy = pyboy - - # plugins_enabled - self.window_sdl2 = WindowSDL2(pyboy, mb, pyboy_argv) - self.window_sdl2_enabled = self.window_sdl2.enabled() - self.window_open_gl = WindowOpenGL(pyboy, mb, pyboy_argv) - self.window_open_gl_enabled = self.window_open_gl.enabled() - self.window_headless = WindowHeadless(pyboy, mb, pyboy_argv) - self.window_headless_enabled = self.window_headless.enabled() - self.window_dummy = WindowDummy(pyboy, mb, pyboy_argv) - self.window_dummy_enabled = self.window_dummy.enabled() - self.debug = Debug(pyboy, mb, pyboy_argv) - self.debug_enabled = self.debug.enabled() - self.disable_input = DisableInput(pyboy, mb, pyboy_argv) - self.disable_input_enabled = self.disable_input.enabled() - self.auto_pause = AutoPause(pyboy, mb, pyboy_argv) - self.auto_pause_enabled = self.auto_pause.enabled() - self.record_replay = RecordReplay(pyboy, mb, pyboy_argv) - self.record_replay_enabled = self.record_replay.enabled() - self.rewind = Rewind(pyboy, mb, pyboy_argv) - self.rewind_enabled = self.rewind.enabled() - self.screen_recorder = ScreenRecorder(pyboy, mb, pyboy_argv) - self.screen_recorder_enabled = self.screen_recorder.enabled() - self.screenshot_recorder = ScreenshotRecorder(pyboy, mb, pyboy_argv) - self.screenshot_recorder_enabled = self.screenshot_recorder.enabled() - self.game_wrapper_super_mario_land = GameWrapperSuperMarioLand(pyboy, mb, pyboy_argv) - self.game_wrapper_super_mario_land_enabled = self.game_wrapper_super_mario_land.enabled() - self.game_wrapper_tetris = GameWrapperTetris(pyboy, mb, pyboy_argv) - self.game_wrapper_tetris_enabled = self.game_wrapper_tetris.enabled() - self.game_wrapper_kirby_dream_land = GameWrapperKirbyDreamLand(pyboy, mb, pyboy_argv) - self.game_wrapper_kirby_dream_land_enabled = self.game_wrapper_kirby_dream_land.enabled() - # plugins_enabled end - - def gamewrapper(self): - # gamewrapper - if self.game_wrapper_super_mario_land_enabled: - return self.game_wrapper_super_mario_land - if self.game_wrapper_tetris_enabled: - return self.game_wrapper_tetris - if self.game_wrapper_kirby_dream_land_enabled: - return self.game_wrapper_kirby_dream_land - # gamewrapper end - return None - - def handle_events(self, events): - # foreach windows events = [].handle_events(events) - if self.window_sdl2_enabled: - events = self.window_sdl2.handle_events(events) - if self.window_open_gl_enabled: - events = self.window_open_gl.handle_events(events) - if self.window_headless_enabled: - events = self.window_headless.handle_events(events) - if self.window_dummy_enabled: - events = self.window_dummy.handle_events(events) - if self.debug_enabled: - events = self.debug.handle_events(events) - # foreach end - # foreach plugins events = [].handle_events(events) - if self.disable_input_enabled: - events = self.disable_input.handle_events(events) - if self.auto_pause_enabled: - events = self.auto_pause.handle_events(events) - if self.record_replay_enabled: - events = self.record_replay.handle_events(events) - if self.rewind_enabled: - events = self.rewind.handle_events(events) - if self.screen_recorder_enabled: - events = self.screen_recorder.handle_events(events) - if self.screenshot_recorder_enabled: - events = self.screenshot_recorder.handle_events(events) - if self.game_wrapper_super_mario_land_enabled: - events = self.game_wrapper_super_mario_land.handle_events(events) - if self.game_wrapper_tetris_enabled: - events = self.game_wrapper_tetris.handle_events(events) - if self.game_wrapper_kirby_dream_land_enabled: - events = self.game_wrapper_kirby_dream_land.handle_events(events) - # foreach end - return events - - def post_tick(self): - # foreach plugins [].post_tick() - if self.disable_input_enabled: - self.disable_input.post_tick() - if self.auto_pause_enabled: - self.auto_pause.post_tick() - if self.record_replay_enabled: - self.record_replay.post_tick() - if self.rewind_enabled: - self.rewind.post_tick() - if self.screen_recorder_enabled: - self.screen_recorder.post_tick() - if self.screenshot_recorder_enabled: - self.screenshot_recorder.post_tick() - if self.game_wrapper_super_mario_land_enabled: - self.game_wrapper_super_mario_land.post_tick() - if self.game_wrapper_tetris_enabled: - self.game_wrapper_tetris.post_tick() - if self.game_wrapper_kirby_dream_land_enabled: - self.game_wrapper_kirby_dream_land.post_tick() - # foreach end - - self._post_tick_windows() - self._set_title() - - def _set_title(self): - # foreach windows [].set_title(self.pyboy.window_title) - if self.window_sdl2_enabled: - self.window_sdl2.set_title(self.pyboy.window_title) - if self.window_open_gl_enabled: - self.window_open_gl.set_title(self.pyboy.window_title) - if self.window_headless_enabled: - self.window_headless.set_title(self.pyboy.window_title) - if self.window_dummy_enabled: - self.window_dummy.set_title(self.pyboy.window_title) - if self.debug_enabled: - self.debug.set_title(self.pyboy.window_title) - # foreach end - pass - - def _post_tick_windows(self): - # foreach windows [].post_tick() - if self.window_sdl2_enabled: - self.window_sdl2.post_tick() - if self.window_open_gl_enabled: - self.window_open_gl.post_tick() - if self.window_headless_enabled: - self.window_headless.post_tick() - if self.window_dummy_enabled: - self.window_dummy.post_tick() - if self.debug_enabled: - self.debug.post_tick() - # foreach end - pass - - def frame_limiter(self, speed): - if speed <= 0: - return - # foreach windows done = [].frame_limiter(speed), if done: return - if self.window_sdl2_enabled: - done = self.window_sdl2.frame_limiter(speed) - if done: - return - if self.window_open_gl_enabled: - done = self.window_open_gl.frame_limiter(speed) - if done: - return - if self.window_headless_enabled: - done = self.window_headless.frame_limiter(speed) - if done: - return - if self.window_dummy_enabled: - done = self.window_dummy.frame_limiter(speed) - if done: - return - if self.debug_enabled: - done = self.debug.frame_limiter(speed) - if done: - return - # foreach end - - def window_title(self): - title = "" - # foreach windows title += [].window_title() - if self.window_sdl2_enabled: - title += self.window_sdl2.window_title() - if self.window_open_gl_enabled: - title += self.window_open_gl.window_title() - if self.window_headless_enabled: - title += self.window_headless.window_title() - if self.window_dummy_enabled: - title += self.window_dummy.window_title() - if self.debug_enabled: - title += self.debug.window_title() - # foreach end - # foreach plugins title += [].window_title() - if self.disable_input_enabled: - title += self.disable_input.window_title() - if self.auto_pause_enabled: - title += self.auto_pause.window_title() - if self.record_replay_enabled: - title += self.record_replay.window_title() - if self.rewind_enabled: - title += self.rewind.window_title() - if self.screen_recorder_enabled: - title += self.screen_recorder.window_title() - if self.screenshot_recorder_enabled: - title += self.screenshot_recorder.window_title() - if self.game_wrapper_super_mario_land_enabled: - title += self.game_wrapper_super_mario_land.window_title() - if self.game_wrapper_tetris_enabled: - title += self.game_wrapper_tetris.window_title() - if self.game_wrapper_kirby_dream_land_enabled: - title += self.game_wrapper_kirby_dream_land.window_title() - # foreach end - return title - - def stop(self): - # foreach windows [].stop() - if self.window_sdl2_enabled: - self.window_sdl2.stop() - if self.window_open_gl_enabled: - self.window_open_gl.stop() - if self.window_headless_enabled: - self.window_headless.stop() - if self.window_dummy_enabled: - self.window_dummy.stop() - if self.debug_enabled: - self.debug.stop() - # foreach end - # foreach plugins [].stop() - if self.disable_input_enabled: - self.disable_input.stop() - if self.auto_pause_enabled: - self.auto_pause.stop() - if self.record_replay_enabled: - self.record_replay.stop() - if self.rewind_enabled: - self.rewind.stop() - if self.screen_recorder_enabled: - self.screen_recorder.stop() - if self.screenshot_recorder_enabled: - self.screenshot_recorder.stop() - if self.game_wrapper_super_mario_land_enabled: - self.game_wrapper_super_mario_land.stop() - if self.game_wrapper_tetris_enabled: - self.game_wrapper_tetris.stop() - if self.game_wrapper_kirby_dream_land_enabled: - self.game_wrapper_kirby_dream_land.stop() - # foreach end - pass - - def handle_breakpoint(self): - if self.debug_enabled: - self.debug.handle_breakpoint() diff --git a/pyboy/plugins/manager_gen.py b/pyboy/plugins/manager_gen.py deleted file mode 100644 index 4710abdf4..000000000 --- a/pyboy/plugins/manager_gen.py +++ /dev/null @@ -1,187 +0,0 @@ -# -# License: See LICENSE.md file -# GitHub: https://github.com/Baekalfen/PyBoy -# -import re - -# Plugins and priority! -# E.g. DisableInput first -windows = ["WindowSDL2", "WindowOpenGL", "WindowHeadless", "WindowDummy", "Debug"] -game_wrappers = ["GameWrapperSuperMarioLand", "GameWrapperTetris", "GameWrapperKirbyDreamLand"] -plugins = [ - "DisableInput", "AutoPause", "RecordReplay", "Rewind", "ScreenRecorder", "ScreenshotRecorder" -] + game_wrappers -all_plugins = windows + plugins - - -def to_snake_case(s): - s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", s) - return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() - - -def skip_lines(iterator, stop): - # Skip old lines - while True: - if next(line_iter).strip().startswith(stop): - break - - -if __name__ == "__main__": - out_lines = [] - with open("manager.py", "r") as f: - line_iter = iter(f.readlines()) - while True: - line = next(line_iter, None) - if line is None: - break - - # Find place to inject - if line.strip().startswith("# foreach"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# foreach") - - skip_lines(line_iter, "# foreach end") - - _, foreach, plugin_type, fun = line.strip().split(" ", 3) - for p in eval(plugin_type): - p_name = to_snake_case(p) - lines.append(f"if self.{p_name}_enabled:\n") - # lines.append(f" {var_name} = self.{p_name}\n") - for sub_fun in fun.split(", "): - sub_fun = sub_fun.replace("[]", f"self.{p_name}") - lines.append(f" {sub_fun}\n") - - lines.append("# foreach end\n") - out_lines.extend([indentation + l for l in lines]) - elif line.strip().startswith("# plugins_enabled"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# plugins_enabled") - - skip_lines(line_iter, "# plugins_enabled end") - - for p in all_plugins: - p_name = to_snake_case(p) - lines.append(f"self.{p_name} = {p}(pyboy, mb, pyboy_argv)\n") - lines.append(f"self.{p_name}_enabled = self.{p_name}.enabled()\n") - - lines.append("# plugins_enabled end\n") - out_lines.extend([indentation + l for l in lines]) - elif line.strip().startswith("# yield_plugins"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# yield_plugins") - - skip_lines(line_iter, "# yield_plugins end") - - for p in all_plugins: - p_name = to_snake_case(p) - lines.append(f"yield {p}.argv\n") - - lines.append("# yield_plugins end\n") - out_lines.extend([indentation + l for l in lines]) - elif line.strip().startswith("# imports"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# imports") - - skip_lines(line_iter, "# imports end") - - for p in all_plugins: - p_name = to_snake_case(p) - lines.append(f"from pyboy.plugins.{p_name} import {p} # isort:skip\n") - - lines.append("# imports end\n") - out_lines.extend([indentation + l for l in lines]) - elif line.strip().startswith("# gamewrapper"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# gamewrapper") - - skip_lines(line_iter, "# gamewrapper end") - - for p in game_wrappers: - p_name = to_snake_case(p) - lines.append(f"if self.{p_name}_enabled: return self.{p_name}\n") - - lines.append("# gamewrapper end\n") - out_lines.extend([indentation + l for l in lines]) - else: - out_lines.append(line) - - with open("manager.py", "w") as f: - f.writelines(out_lines) - - out_lines = [] - with open("manager.pxd", "r") as f: - line_iter = iter(f.readlines()) - while True: - line = next(line_iter, None) - if line is None: - break - - # Find place to inject - if line.strip().startswith("# plugin_cdef"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# plugin_cdef") - - skip_lines(line_iter, "# plugin_cdef end") - - for p in all_plugins: - p_name = to_snake_case(p) - lines.append(f"cdef public {p} {p_name}\n") - - for p in all_plugins: - p_name = to_snake_case(p) - lines.append(f"cdef bint {p_name}_enabled\n") - - lines.append("# plugin_cdef end\n") - out_lines.extend([indentation + l for l in lines]) - elif line.strip().startswith("# imports"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# imports") - - skip_lines(line_iter, "# imports end") - - for p in all_plugins: - p_name = to_snake_case(p) - lines.append(f"from pyboy.plugins.{p_name} cimport {p}\n") - - lines.append("# imports end\n") - out_lines.extend([indentation + l for l in lines]) - else: - out_lines.append(line) - - with open("manager.pxd", "w") as f: - f.writelines(out_lines) - - out_lines = [] - with open("__init__.py", "r") as f: - line_iter = iter(f.readlines()) - while True: - line = next(line_iter, None) - if line is None: - break - - # Find place to inject - if line.strip().startswith("# docs exclude"): - - lines = [line.strip() + "\n"] - indentation = " " * line.index("# docs exclude") - - skip_lines(line_iter, "# docs exclude end") - - for p in (set(all_plugins) - set(game_wrappers)) | set(["manager", "manager_gen"]): - p_name = to_snake_case(p) - lines.append(f"\"{p_name}\": False,\n") - - lines.append("# docs exclude end\n") - out_lines.extend([indentation + l for l in lines]) - else: - out_lines.append(line) - - with open("__init__.py", "w") as f: - f.writelines(out_lines) diff --git a/pyboy/plugins/record_replay.py b/pyboy/plugins/record_replay.py index 778aeb4e7..8371743ab 100644 --- a/pyboy/plugins/record_replay.py +++ b/pyboy/plugins/record_replay.py @@ -22,9 +22,6 @@ class RecordReplay(PyBoyPlugin): def __init__(self, *args): super().__init__(*args) - if not self.enabled(): - return - if not self.pyboy_argv.get("loadstate"): logger.warning( "To replay input consistently later, it is recommended to load a state at boot. This will be" @@ -52,8 +49,9 @@ def stop(self): self.recorded_input, ) - def enabled(self): - return self.pyboy_argv.get("record_input") + @classmethod + def enabled(cls, pyboy, pyboy_argv): + return pyboy_argv.get("record_input") def save_replay(rom, loadstate, replay_file, recorded_input): diff --git a/pyboy/plugins/rewind.py b/pyboy/plugins/rewind.py index cc3eae197..f5eb26383 100644 --- a/pyboy/plugins/rewind.py +++ b/pyboy/plugins/rewind.py @@ -73,8 +73,9 @@ def handle_events(self, events): self.pyboy.set_emulation_speed(int(self.rewind_speed)) return events - def enabled(self): - return self.pyboy_argv.get("rewind") + @classmethod + def enabled(cls, pyboy, pyboy_argv): + return pyboy_argv.get("rewind") ############################################################## diff --git a/pyboy/plugins/screen_recorder.py b/pyboy/plugins/screen_recorder.py index 07965c353..58938d034 100644 --- a/pyboy/plugins/screen_recorder.py +++ b/pyboy/plugins/screen_recorder.py @@ -72,7 +72,8 @@ def save(self, path=None, fps=60): logger.error("Screen recording failed: no frames") self.frames = [] - def enabled(self): + @classmethod + def enabled(cls, pyboy, pyboy_argv): if Image is None: logger.warning(f"{__name__}: Missing dependency \"Pillow\". Recording disabled") return False diff --git a/pyboy/plugins/screenshot_recorder.py b/pyboy/plugins/screenshot_recorder.py index d1d8e0b50..dff4586bd 100644 --- a/pyboy/plugins/screenshot_recorder.py +++ b/pyboy/plugins/screenshot_recorder.py @@ -41,7 +41,8 @@ def save(self, path=None): logger.info("Screenshot saved in {}".format(path)) - def enabled(self): + @classmethod + def enabled(cls, pyboy, pyboy_argv): if Image is None: logger.warning(f"{__name__}: Missing dependency \"Pillow\". Screenshots disabled") return False diff --git a/pyboy/plugins/window_dummy.py b/pyboy/plugins/window_dummy.py index efed0f5cb..e7d71c5b4 100644 --- a/pyboy/plugins/window_dummy.py +++ b/pyboy/plugins/window_dummy.py @@ -11,19 +11,19 @@ class WindowDummy(PyBoyWindowPlugin): + name = "dummy" + def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) - if not self.enabled(): - return - pyboy._rendering(False) logger.warning( 'This window type does not support frame-limiting. `pyboy.set_emulation_speed(...)` will have no effect, as it\'s always running at full speed.' ) - def enabled(self): - return self.pyboy_argv.get("window_type") == "dummy" + @classmethod + def enabled(cls, pyboy, pyboy_argv): + return pyboy_argv.get("window_type") == "dummy" def set_title(self, title): logger.info(title) diff --git a/pyboy/plugins/window_headless.py b/pyboy/plugins/window_headless.py index 4fce9b7e7..6a61a02c7 100644 --- a/pyboy/plugins/window_headless.py +++ b/pyboy/plugins/window_headless.py @@ -11,18 +11,18 @@ class WindowHeadless(PyBoyWindowPlugin): + name = "headless" + def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) - if not self.enabled(): - return - logger.warning( 'This window type does not support frame-limiting. `pyboy.set_emulation_speed(...)` will have no effect, as it\'s always running at full speed.' ) - def enabled(self): - return self.pyboy_argv.get("window_type") == "headless" + @classmethod + def enabled(cls, pyboy, pyboy_argv): + return pyboy_argv.get("window_type") == "headless" def set_title(self, title): logger.info(title) diff --git a/pyboy/plugins/window_open_gl.py b/pyboy/plugins/window_open_gl.py index 7d4e6406d..8f5e59ac1 100644 --- a/pyboy/plugins/window_open_gl.py +++ b/pyboy/plugins/window_open_gl.py @@ -32,12 +32,11 @@ class WindowOpenGL(PyBoyWindowPlugin): + name = "OpenGL" + def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) - if not self.enabled(): - return - if not glutInit(): raise Exception("OpenGL couldn't initialize!") glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA) @@ -141,8 +140,9 @@ def _gldraw(self): glDrawPixels(COLS, ROWS, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8, buf) glFlush() - def enabled(self): - if self.pyboy_argv.get("window_type") == "OpenGL": + @classmethod + def enabled(cls, pyboy, pyboy_argv): + if pyboy_argv.get("window_type") == "OpenGL": if opengl_enabled: return True else: diff --git a/pyboy/plugins/window_sdl2.pxd b/pyboy/plugins/window_sdl2.pxd index b25257d26..88f0fca0c 100644 --- a/pyboy/plugins/window_sdl2.pxd +++ b/pyboy/plugins/window_sdl2.pxd @@ -30,4 +30,4 @@ cdef class WindowSDL2(PyBoyWindowPlugin): cdef object _sdltexturebuffer @cython.locals(now=float, delay=cython.int) - cdef bint frame_limiter(self, int) + cpdef bint frame_limiter(self, int) diff --git a/pyboy/plugins/window_sdl2.py b/pyboy/plugins/window_sdl2.py index a0c80195d..d10f6a7d0 100644 --- a/pyboy/plugins/window_sdl2.py +++ b/pyboy/plugins/window_sdl2.py @@ -150,12 +150,11 @@ def sdl2_event_pump(events): class WindowSDL2(PyBoyWindowPlugin): + name = "SDL2" + def __init__(self, pyboy, mb, pyboy_argv): super().__init__(pyboy, mb, pyboy_argv) - if not self.enabled(): - return - sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO | sdl2.SDL_INIT_GAMECONTROLLER) self._ftime = 0.0 @@ -193,8 +192,9 @@ def post_tick(self): sdl2.SDL_RenderPresent(self._sdlrenderer) sdl2.SDL_RenderClear(self._sdlrenderer) - def enabled(self): - if self.pyboy_argv.get("window_type") in ("SDL2", None): + @classmethod + def enabled(cls, pyboy, pyboy_argv): + if pyboy_argv.get("window_type") in ("SDL2", None): if not sdl2: logger.error("Failed to import sdl2, needed for sdl2 window") return False # Disable, or raise exception? diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index e435eda61..e86e454ed 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -9,7 +9,7 @@ cimport cython from libc.stdint cimport uint64_t from pyboy.core.mb cimport Motherboard from pyboy.utils cimport IntIOWrapper, IntIOInterface -from pyboy.plugins.manager cimport PluginManager +from pyboy.plugin_manager cimport PluginManager cdef float SPF diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index a947e68b8..174473798 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -12,7 +12,7 @@ from pyboy.openai_gym import PyBoyGymEnv from pyboy.openai_gym import enabled as gym_enabled -from pyboy.plugins.manager import PluginManager +from pyboy.plugin_manager import PluginManager from pyboy.utils import IntIOWrapper, WindowEvent from . import botsupport diff --git a/setup.py b/setup.py index 93f0c6468..aadcdc747 100644 --- a/setup.py +++ b/setup.py @@ -206,7 +206,7 @@ def get_export_symbols(self, ext): version="v1.5.1", packages=find_packages(), author="Mads Ynddal", - author_email="mads-pyboy@ynddal.dk", + author_email="mads@ynddal.dk", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/Baekalfen/PyBoy", diff --git a/tests/test_basics.py b/tests/test_basics.py index 9220f3a8e..cb3073443 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -34,7 +34,7 @@ def test_record_replay(boot_rom, default_rom): pyboy.send_input(WindowEvent.PRESS_ARROW_UP) pyboy.tick() - events = pyboy.plugin_manager.record_replay.recorded_input + events = pyboy.plugin_manager.get_plugin("RecordReplay").recorded_input assert len(events) == 4, "We assumed only 4 frames were recorded, as frames without events are skipped." frame_no, keys, frame_data = events[0] assert frame_no == 1, "We inserted the key on the second frame" @@ -86,7 +86,6 @@ def test_argv_parser(*args): empty = parser.parse_args(file_that_exists.split(" ")).__dict__ for k, v in { "ROM": file_that_exists, - "autopause": False, "bootrom": None, "debug": False, "loadstate": None, @@ -109,16 +108,9 @@ def test_argv_parser(*args): # Check flags become True flags = parser.parse_args( - f"{file_that_exists} --debug --autopause --profiling --rewind --no-input --log-level INFO".split(" ") + f"{file_that_exists} --debug --profiling --rewind --no-input --log-level INFO".split(" ") ).__dict__ - for k, v in { - "autopause": True, - "debug": True, - "no_input": True, - "log_level": "INFO", - "profiling": True, - "rewind": True - }.items(): + for k, v in {"debug": True, "no_input": True, "log_level": "INFO", "profiling": True, "rewind": True}.items(): assert flags[k] == v