From aef880b6decc4d4a1f4c8101ca7d8bbe4dd83051 Mon Sep 17 00:00:00 2001 From: Daivik Bhatia Date: Thu, 12 Sep 2024 21:28:28 +0200 Subject: [PATCH] Introduce Gameshark to API Co-authored-by: Artucuno Co-authored-by: Mads Ynddal --- docs/api/gameshark.html | 522 ++++++++++++++++++++++++++++++++++++++++ docs/api/index.html | 6 + docs/index.html | 29 ++- pyboy/__main__.py | 5 + pyboy/api/__init__.py | 1 + pyboy/api/gameshark.pxd | 24 ++ pyboy/api/gameshark.py | 153 ++++++++++++ pyboy/pyboy.pxd | 2 + pyboy/pyboy.py | 18 ++ tests/test_gameshark.py | 65 +++++ 10 files changed, 824 insertions(+), 1 deletion(-) create mode 100644 docs/api/gameshark.html create mode 100644 pyboy/api/gameshark.pxd create mode 100644 pyboy/api/gameshark.py create mode 100644 tests/test_gameshark.py diff --git a/docs/api/gameshark.html b/docs/api/gameshark.html new file mode 100644 index 000000000..0d2829ea1 --- /dev/null +++ b/docs/api/gameshark.html @@ -0,0 +1,522 @@ + + + + + + +pyboy.api.gameshark API documentation + + + + + + + + + + +
+
+
+

Module pyboy.api.gameshark

+
+
+
+ +Expand source code + +
#
+# License: See LICENSE.md file
+# GitHub: https://github.com/Baekalfen/PyBoy
+#
+
+from pyboy.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+class GameShark:
+    def __init__(self, memory):
+        self.memory = memory
+        self.cheats = {}
+        self.enabled = False
+
+    def _convert_cheat(self, gameshark_code):
+        '''
+        A GameShark code for these consoles is written in the format ttvvaaaa. tt specifies the type, which is usually 01.
+        vv specifies the hexadecimal value the code will write into the game's memory. aaaa specifies the memory address
+        that will be modified, with the low byte first (e.g. address C056 is written as 56C0).
+        Example 011556C0 would output:
+        type = 01
+        value = 0x15
+        address = 0x56C0
+
+        For more details:
+        https://doc.kodewerx.org/hacking_gb.html
+
+        There seems to be conflicting information about the presence of other types than 01.
+        '''
+        # Check if the input cheat code has the correct length (8 characters)
+        if len(gameshark_code) != 8:
+            raise ValueError("Invalid cheat code length. Cheat code must be 8 characters long.")
+
+        # Extract components from the cheat code
+        _type = int(gameshark_code[:2], 16)
+        value = int(gameshark_code[2:4], 16) # Convert hexadecimal value to an integer
+        unconverted_address = gameshark_code[4:] # Ex:   1ED1
+        lower = unconverted_address[:2] # Ex:  1E
+        upper = unconverted_address[2:] # Ex:  D1
+        address_converted = upper + lower # Ex: 0xD11E   # Converting to Ram Readable address
+        address = int(address_converted, 16)
+
+        if not 0x8000 <= address:
+            raise ValueError("Invalid GameShark code provided. Address not in the RAM range")
+
+        return (_type, value, address)
+
+    def _get_value(self, _type, address):
+        if _type == 0x01:
+            # 8-bit RAM write
+            # Writes the byte xx to the address zzyy.
+            return self.memory[address]
+        # elif (_type & 0xF0) == 0x80:
+        #     # 8-bit RAM write (with bank change)
+        #     # Changes the RAM bank to b, then writes the byte xx to the address zzyy.
+        #     bank = _type & 0xF
+        #     pass
+        # elif (_type & 0xF0) == 0x90:
+        #     # 8-bit RAM write (with WRAM bank change)
+        #     # Changes the WRAM bank to b and then writes the byte xx to the address zzyy. GBC only.
+        #     bank = _type & 0xF
+        #     pass
+        else:
+            raise ValueError("Invalid GameShark type", _type)
+
+    def _set_value(self, _type, value, address):
+        if _type == 0x01:
+            # 8-bit RAM write
+            # Writes the byte xx to the address zzyy.
+            self.memory[address] = value
+        # elif (_type & 0xF0) == 0x80:
+        #     # 8-bit RAM write (with bank change)
+        #     # Changes the RAM bank to b, then writes the byte xx to the address zzyy.
+        #     bank = _type & 0xF
+        #     pass
+        # elif (_type & 0xF0) == 0x90:
+        #     # 8-bit RAM write (with WRAM bank change)
+        #     # Changes the WRAM bank to b and then writes the byte xx to the address zzyy. GBC only.
+        #     bank = _type & 0xF
+        #     pass
+        else:
+            raise ValueError("Invalid GameShark type", _type)
+
+    def add(self, code):
+        """
+        Add a GameShark cheat to the emulator.
+
+        Example:
+        ```python
+        >>> pyboy.gameshark.add("01FF16D0")
+        ```
+
+        Args:
+            code (str): GameShark code to add
+        """
+        self.enabled = True
+        _type, value, address = self._convert_cheat(code)
+        if code not in self.cheats:
+            self.cheats[code] = (self._get_value(_type, address), (_type, value, address))
+        else:
+            logger.error("GameShark code already applied!")
+
+    def remove(self, code, restore_value=True):
+        """
+        Remove a GameShark cheat from the emulator.
+
+        Example:
+        ```python
+        >>> pyboy.gameshark.add("01FF16D0")
+        >>> pyboy.gameshark.remove("01FF16D0")
+        ```
+
+        Args:
+            code (str): GameShark code to remove
+            restore_value (bool): True to restore original value at address, otherwise don't restore
+        """
+
+        if not code in self.cheats:
+            raise ValueError("GameShark code cannot be removed. Hasn't been applied.")
+
+        original_value, (_type, _, address) = self.cheats.pop(code)
+        if restore_value:
+            self._set_value(_type, original_value, address)
+
+        if len(self.cheats) == 0:
+            self.enabled = False
+
+    def clear_all(self, restore_value=True):
+        """
+        Remove all GameShark cheats from the emulator.
+
+        Example:
+        ```python
+        >>> pyboy.gameshark.clear_all()
+        ```
+
+        Args:
+            restore_value (bool): Restore the original values of the memory addresses that were modified by the cheats.
+        """
+        # NOTE: Create a list so we don't remove from the iterator we are going through
+        for code in list(self.cheats.keys()):
+            self.remove(code, restore_value)
+
+    def tick(self):
+        if not self.enabled:
+            return
+        # https://gbdev.io/pandocs/Shark_Cheats.html
+        # "As far as it is understood, patching is implemented by hooking the original VBlank interrupt handler, and
+        # re-writing RAM values each frame."
+        for _, (_, (_type, value, address)) in self.cheats.items():
+            self._set_value(_type, value, address)
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class GameShark +(memory) +
+
+
+
+ +Expand source code + +
class GameShark:
+    def __init__(self, memory):
+        self.memory = memory
+        self.cheats = {}
+        self.enabled = False
+
+    def _convert_cheat(self, gameshark_code):
+        '''
+        A GameShark code for these consoles is written in the format ttvvaaaa. tt specifies the type, which is usually 01.
+        vv specifies the hexadecimal value the code will write into the game's memory. aaaa specifies the memory address
+        that will be modified, with the low byte first (e.g. address C056 is written as 56C0).
+        Example 011556C0 would output:
+        type = 01
+        value = 0x15
+        address = 0x56C0
+
+        For more details:
+        https://doc.kodewerx.org/hacking_gb.html
+
+        There seems to be conflicting information about the presence of other types than 01.
+        '''
+        # Check if the input cheat code has the correct length (8 characters)
+        if len(gameshark_code) != 8:
+            raise ValueError("Invalid cheat code length. Cheat code must be 8 characters long.")
+
+        # Extract components from the cheat code
+        _type = int(gameshark_code[:2], 16)
+        value = int(gameshark_code[2:4], 16) # Convert hexadecimal value to an integer
+        unconverted_address = gameshark_code[4:] # Ex:   1ED1
+        lower = unconverted_address[:2] # Ex:  1E
+        upper = unconverted_address[2:] # Ex:  D1
+        address_converted = upper + lower # Ex: 0xD11E   # Converting to Ram Readable address
+        address = int(address_converted, 16)
+
+        if not 0x8000 <= address:
+            raise ValueError("Invalid GameShark code provided. Address not in the RAM range")
+
+        return (_type, value, address)
+
+    def _get_value(self, _type, address):
+        if _type == 0x01:
+            # 8-bit RAM write
+            # Writes the byte xx to the address zzyy.
+            return self.memory[address]
+        # elif (_type & 0xF0) == 0x80:
+        #     # 8-bit RAM write (with bank change)
+        #     # Changes the RAM bank to b, then writes the byte xx to the address zzyy.
+        #     bank = _type & 0xF
+        #     pass
+        # elif (_type & 0xF0) == 0x90:
+        #     # 8-bit RAM write (with WRAM bank change)
+        #     # Changes the WRAM bank to b and then writes the byte xx to the address zzyy. GBC only.
+        #     bank = _type & 0xF
+        #     pass
+        else:
+            raise ValueError("Invalid GameShark type", _type)
+
+    def _set_value(self, _type, value, address):
+        if _type == 0x01:
+            # 8-bit RAM write
+            # Writes the byte xx to the address zzyy.
+            self.memory[address] = value
+        # elif (_type & 0xF0) == 0x80:
+        #     # 8-bit RAM write (with bank change)
+        #     # Changes the RAM bank to b, then writes the byte xx to the address zzyy.
+        #     bank = _type & 0xF
+        #     pass
+        # elif (_type & 0xF0) == 0x90:
+        #     # 8-bit RAM write (with WRAM bank change)
+        #     # Changes the WRAM bank to b and then writes the byte xx to the address zzyy. GBC only.
+        #     bank = _type & 0xF
+        #     pass
+        else:
+            raise ValueError("Invalid GameShark type", _type)
+
+    def add(self, code):
+        """
+        Add a GameShark cheat to the emulator.
+
+        Example:
+        ```python
+        >>> pyboy.gameshark.add("01FF16D0")
+        ```
+
+        Args:
+            code (str): GameShark code to add
+        """
+        self.enabled = True
+        _type, value, address = self._convert_cheat(code)
+        if code not in self.cheats:
+            self.cheats[code] = (self._get_value(_type, address), (_type, value, address))
+        else:
+            logger.error("GameShark code already applied!")
+
+    def remove(self, code, restore_value=True):
+        """
+        Remove a GameShark cheat from the emulator.
+
+        Example:
+        ```python
+        >>> pyboy.gameshark.add("01FF16D0")
+        >>> pyboy.gameshark.remove("01FF16D0")
+        ```
+
+        Args:
+            code (str): GameShark code to remove
+            restore_value (bool): True to restore original value at address, otherwise don't restore
+        """
+
+        if not code in self.cheats:
+            raise ValueError("GameShark code cannot be removed. Hasn't been applied.")
+
+        original_value, (_type, _, address) = self.cheats.pop(code)
+        if restore_value:
+            self._set_value(_type, original_value, address)
+
+        if len(self.cheats) == 0:
+            self.enabled = False
+
+    def clear_all(self, restore_value=True):
+        """
+        Remove all GameShark cheats from the emulator.
+
+        Example:
+        ```python
+        >>> pyboy.gameshark.clear_all()
+        ```
+
+        Args:
+            restore_value (bool): Restore the original values of the memory addresses that were modified by the cheats.
+        """
+        # NOTE: Create a list so we don't remove from the iterator we are going through
+        for code in list(self.cheats.keys()):
+            self.remove(code, restore_value)
+
+    def tick(self):
+        if not self.enabled:
+            return
+        # https://gbdev.io/pandocs/Shark_Cheats.html
+        # "As far as it is understood, patching is implemented by hooking the original VBlank interrupt handler, and
+        # re-writing RAM values each frame."
+        for _, (_, (_type, value, address)) in self.cheats.items():
+            self._set_value(_type, value, address)
+
+

Methods

+
+
+def add(self, code) +
+
+

Add a GameShark cheat to the emulator.

+

Example:

+
>>> pyboy.gameshark.add("01FF16D0")
+
+

Args

+
+
code : str
+
GameShark code to add
+
+
+ +Expand source code + +
def add(self, code):
+    """
+    Add a GameShark cheat to the emulator.
+
+    Example:
+    ```python
+    >>> pyboy.gameshark.add("01FF16D0")
+    ```
+
+    Args:
+        code (str): GameShark code to add
+    """
+    self.enabled = True
+    _type, value, address = self._convert_cheat(code)
+    if code not in self.cheats:
+        self.cheats[code] = (self._get_value(_type, address), (_type, value, address))
+    else:
+        logger.error("GameShark code already applied!")
+
+
+
+def remove(self, code, restore_value=True) +
+
+

Remove a GameShark cheat from the emulator.

+

Example:

+
>>> pyboy.gameshark.add("01FF16D0")
+>>> pyboy.gameshark.remove("01FF16D0")
+
+

Args

+
+
code : str
+
GameShark code to remove
+
restore_value : bool
+
True to restore original value at address, otherwise don't restore
+
+
+ +Expand source code + +
def remove(self, code, restore_value=True):
+    """
+    Remove a GameShark cheat from the emulator.
+
+    Example:
+    ```python
+    >>> pyboy.gameshark.add("01FF16D0")
+    >>> pyboy.gameshark.remove("01FF16D0")
+    ```
+
+    Args:
+        code (str): GameShark code to remove
+        restore_value (bool): True to restore original value at address, otherwise don't restore
+    """
+
+    if not code in self.cheats:
+        raise ValueError("GameShark code cannot be removed. Hasn't been applied.")
+
+    original_value, (_type, _, address) = self.cheats.pop(code)
+    if restore_value:
+        self._set_value(_type, original_value, address)
+
+    if len(self.cheats) == 0:
+        self.enabled = False
+
+
+
+def clear_all(self, restore_value=True) +
+
+

Remove all GameShark cheats from the emulator.

+

Example:

+
>>> pyboy.gameshark.clear_all()
+
+

Args

+
+
restore_value : bool
+
Restore the original values of the memory addresses that were modified by the cheats.
+
+
+ +Expand source code + +
def clear_all(self, restore_value=True):
+    """
+    Remove all GameShark cheats from the emulator.
+
+    Example:
+    ```python
+    >>> pyboy.gameshark.clear_all()
+    ```
+
+    Args:
+        restore_value (bool): Restore the original values of the memory addresses that were modified by the cheats.
+    """
+    # NOTE: Create a list so we don't remove from the iterator we are going through
+    for code in list(self.cheats.keys()):
+        self.remove(code, restore_value)
+
+
+
+def tick(self) +
+
+
+
+ +Expand source code + +
def tick(self):
+    if not self.enabled:
+        return
+    # https://gbdev.io/pandocs/Shark_Cheats.html
+    # "As far as it is understood, patching is implemented by hooking the original VBlank interrupt handler, and
+    # re-writing RAM values each frame."
+    for _, (_, (_type, value, address)) in self.cheats.items():
+        self._set_value(_type, value, address)
+
+
+
+
+
+
+
+ +
+ + + + + \ No newline at end of file diff --git a/docs/api/index.html b/docs/api/index.html index 69d4af7ea..cd35605b7 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -35,6 +35,7 @@

Module pyboy.api

""" from . import constants +from .gameshark import GameShark from .screen import Screen from .sprite import Sprite from .tile import Tile @@ -54,6 +55,10 @@

Sub-modules

Memory constants used internally to calculate tile and tile map addresses.

+
pyboy.api.gameshark
+
+
+
pyboy.api.memory_scanner
@@ -98,6 +103,7 @@

Index

  • Sub-modules

    • pyboy.api.constants
    • +
    • pyboy.api.gameshark
    • pyboy.api.memory_scanner
    • pyboy.api.screen
    • pyboy.api.sprite
    • diff --git a/docs/index.html b/docs/index.html index 30dc01166..445d1fc30 100644 --- a/docs/index.html +++ b/docs/index.html @@ -68,7 +68,7 @@

      Classes

      class PyBoy -(gamerom, *, window='SDL2', scale=3, symbols=None, bootrom=None, sound=False, sound_emulated=False, cgb=None, log_level='ERROR', **kwargs) +(gamerom, *, window='SDL2', scale=3, symbols=None, bootrom=None, sound=False, sound_emulated=False, cgb=None, gameshark=None, log_level='ERROR', **kwargs)

      PyBoy is loadable as an object in Python. This means, it can be initialized from another script, and be @@ -121,6 +121,7 @@

      Kwargs

      sound=False, sound_emulated=False, cgb=None, + gameshark=None, log_level=defaults["log_level"], **kwargs ): @@ -426,6 +427,21 @@

      Kwargs

      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): @@ -436,6 +452,7 @@

      Kwargs

      self._handle_events(self.events) t_pre = time.perf_counter_ns() if not self.paused: + self.gameshark.tick() self.__rendering(render) # Reenter mb.tick until we eventually get a clean exit without breakpoints while self.mb.tick(): @@ -1490,6 +1507,15 @@

      Returns

      PyBoyGameWrapper: A game-specific wrapper object.

      +
      var gameshark
      +
      +

      Provides an instance of the GameShark handler. This allows you to inject GameShark-based cheat codes.

      +

      Example:

      +
      >>> pyboy.gameshark.add("010138CD")
      +>>> pyboy.gameshark.remove("010138CD")
      +>>> pyboy.gameshark.clear_all()
      +
      +

      Methods

      @@ -3604,6 +3630,7 @@

      PyBoy

    • tilemap_window
    • cartridge_title
    • game_wrapper
    • +
    • gameshark
  • diff --git a/pyboy/__main__.py b/pyboy/__main__.py index dbcc1dfd0..8c5209fd4 100644 --- a/pyboy/__main__.py +++ b/pyboy/__main__.py @@ -87,6 +87,11 @@ def valid_file_path(path): parser.add_argument("-s", "--scale", default=defaults["scale"], type=int, help="The scaling multiplier for the window") parser.add_argument("--sound", action="store_true", help="Enable sound (beta)") parser.add_argument("--no-renderer", action="store_true", help="Disable rendering (internal use)") +parser.add_argument( + "--gameshark", + type=str, + help="Add GameShark cheats on start-up. Add multiple by comma separation (i.e. '010138CD, 01033CD1')" +) gameboy_type_parser = parser.add_mutually_exclusive_group() gameboy_type_parser.add_argument( diff --git a/pyboy/api/__init__.py b/pyboy/api/__init__.py index 2676363fd..964f36905 100644 --- a/pyboy/api/__init__.py +++ b/pyboy/api/__init__.py @@ -7,6 +7,7 @@ """ from . import constants +from .gameshark import GameShark from .screen import Screen from .sprite import Sprite from .tile import Tile diff --git a/pyboy/api/gameshark.pxd b/pyboy/api/gameshark.pxd new file mode 100644 index 000000000..895160651 --- /dev/null +++ b/pyboy/api/gameshark.pxd @@ -0,0 +1,24 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +from libc.stdint cimport uint8_t + +from pyboy.logging.logging cimport Logger + + +cdef Logger logger + +cdef class GameShark: + cdef object memory + cdef dict cheats + cdef bint enabled + + cdef uint8_t _get_value(self, int, int) noexcept + cdef void _set_value(self, int, int, int) noexcept + cdef tuple _convert_cheat(self, str code) noexcept + cpdef void add(self, str code) noexcept + cpdef void remove(self, str code, bint restore_value=*) noexcept + cpdef void clear_all(self, bint restore_value=*) noexcept + cdef void tick(self) noexcept diff --git a/pyboy/api/gameshark.py b/pyboy/api/gameshark.py new file mode 100644 index 000000000..7427ecc0e --- /dev/null +++ b/pyboy/api/gameshark.py @@ -0,0 +1,153 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +from pyboy.logging import get_logger + +logger = get_logger(__name__) + + +class GameShark: + def __init__(self, memory): + self.memory = memory + self.cheats = {} + self.enabled = False + + def _convert_cheat(self, gameshark_code): + ''' + A GameShark code for these consoles is written in the format ttvvaaaa. tt specifies the type, which is usually 01. + vv specifies the hexadecimal value the code will write into the game's memory. aaaa specifies the memory address + that will be modified, with the low byte first (e.g. address C056 is written as 56C0). + Example 011556C0 would output: + type = 01 + value = 0x15 + address = 0x56C0 + + For more details: + https://doc.kodewerx.org/hacking_gb.html + + There seems to be conflicting information about the presence of other types than 01. + ''' + # Check if the input cheat code has the correct length (8 characters) + if len(gameshark_code) != 8: + raise ValueError("Invalid cheat code length. Cheat code must be 8 characters long.") + + # Extract components from the cheat code + _type = int(gameshark_code[:2], 16) + value = int(gameshark_code[2:4], 16) # Convert hexadecimal value to an integer + unconverted_address = gameshark_code[4:] # Ex: 1ED1 + lower = unconverted_address[:2] # Ex: 1E + upper = unconverted_address[2:] # Ex: D1 + address_converted = upper + lower # Ex: 0xD11E # Converting to Ram Readable address + address = int(address_converted, 16) + + if not 0x8000 <= address: + raise ValueError("Invalid GameShark code provided. Address not in the RAM range") + + return (_type, value, address) + + def _get_value(self, _type, address): + if _type == 0x01: + # 8-bit RAM write + # Writes the byte xx to the address zzyy. + return self.memory[address] + # elif (_type & 0xF0) == 0x80: + # # 8-bit RAM write (with bank change) + # # Changes the RAM bank to b, then writes the byte xx to the address zzyy. + # bank = _type & 0xF + # pass + # elif (_type & 0xF0) == 0x90: + # # 8-bit RAM write (with WRAM bank change) + # # Changes the WRAM bank to b and then writes the byte xx to the address zzyy. GBC only. + # bank = _type & 0xF + # pass + else: + raise ValueError("Invalid GameShark type", _type) + + def _set_value(self, _type, value, address): + if _type == 0x01: + # 8-bit RAM write + # Writes the byte xx to the address zzyy. + self.memory[address] = value + # elif (_type & 0xF0) == 0x80: + # # 8-bit RAM write (with bank change) + # # Changes the RAM bank to b, then writes the byte xx to the address zzyy. + # bank = _type & 0xF + # pass + # elif (_type & 0xF0) == 0x90: + # # 8-bit RAM write (with WRAM bank change) + # # Changes the WRAM bank to b and then writes the byte xx to the address zzyy. GBC only. + # bank = _type & 0xF + # pass + else: + raise ValueError("Invalid GameShark type", _type) + + def add(self, code): + """ + Add a GameShark cheat to the emulator. + + Example: + ```python + >>> pyboy.gameshark.add("01FF16D0") + ``` + + Args: + code (str): GameShark code to add + """ + self.enabled = True + _type, value, address = self._convert_cheat(code) + if code not in self.cheats: + self.cheats[code] = (self._get_value(_type, address), (_type, value, address)) + else: + logger.error("GameShark code already applied!") + + def remove(self, code, restore_value=True): + """ + Remove a GameShark cheat from the emulator. + + Example: + ```python + >>> pyboy.gameshark.add("01FF16D0") + >>> pyboy.gameshark.remove("01FF16D0") + ``` + + Args: + code (str): GameShark code to remove + restore_value (bool): True to restore original value at address, otherwise don't restore + """ + + if not code in self.cheats: + raise ValueError("GameShark code cannot be removed. Hasn't been applied.") + + original_value, (_type, _, address) = self.cheats.pop(code) + if restore_value: + self._set_value(_type, original_value, address) + + if len(self.cheats) == 0: + self.enabled = False + + def clear_all(self, restore_value=True): + """ + Remove all GameShark cheats from the emulator. + + Example: + ```python + >>> pyboy.gameshark.clear_all() + ``` + + Args: + restore_value (bool): Restore the original values of the memory addresses that were modified by the cheats. + """ + # NOTE: Create a list so we don't remove from the iterator we are going through + for code in list(self.cheats.keys()): + self.remove(code, restore_value) + + def tick(self): + if not self.enabled: + return + # https://gbdev.io/pandocs/Shark_Cheats.html + # "As far as it is understood, patching is implemented by hooking the original VBlank interrupt handler, and + # re-writing RAM values each frame." + for _, (_, (_type, value, address)) in self.cheats.items(): + self._set_value(_type, value, address) diff --git a/pyboy/pyboy.pxd b/pyboy/pyboy.pxd index fec26265d..700647b68 100644 --- a/pyboy/pyboy.pxd +++ b/pyboy/pyboy.pxd @@ -8,6 +8,7 @@ cimport cython from libc cimport time from libc.stdint cimport int64_t, uint64_t +from pyboy.api.gameshark cimport GameShark from pyboy.api.memory_scanner cimport MemoryScanner from pyboy.api.screen cimport Screen from pyboy.api.tilemap cimport TileMap @@ -59,6 +60,7 @@ cdef class PyBoy: cdef readonly TileMap tilemap_window cdef readonly object game_wrapper cdef readonly MemoryScanner memory_scanner + cdef readonly GameShark gameshark cdef readonly str cartridge_title cdef bint limit_emulationspeed diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index cf09628b2..c9c5b1502 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -13,6 +13,7 @@ import numpy as np +from pyboy.api.gameshark import GameShark from pyboy.api.memory_scanner import MemoryScanner from pyboy.api.screen import Screen from pyboy.api.tilemap import TileMap @@ -50,6 +51,7 @@ def __init__( sound=False, sound_emulated=False, cgb=None, + gameshark=None, log_level=defaults["log_level"], **kwargs ): @@ -355,6 +357,21 @@ def __init__( 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): @@ -365,6 +382,7 @@ def _tick(self, render): self._handle_events(self.events) t_pre = time.perf_counter_ns() if not self.paused: + self.gameshark.tick() self.__rendering(render) # Reenter mb.tick until we eventually get a clean exit without breakpoints while self.mb.tick(): diff --git a/tests/test_gameshark.py b/tests/test_gameshark.py new file mode 100644 index 000000000..8ed196dc9 --- /dev/null +++ b/tests/test_gameshark.py @@ -0,0 +1,65 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +from pyboy import PyBoy + + +def test_gameshark(default_rom): + pyboy = PyBoy(default_rom, window="null") + pyboy.tick(60) + + code = "010138CD" + value = 0x01 + bank = 0x00 + address = 0xCD38 + + # Establish truth + pyboy.tick() + assert pyboy.memory[address] != value + + # Apply cheat, and validate new value + pyboy.gameshark.add(code) + pyboy.tick() + assert pyboy.memory[address] == value + + # Remove cheat, and reestablish old value + pyboy.gameshark.remove(code) + assert pyboy.memory[address] != value + + # Add cheat, remove, but do not reestablish old value + pyboy.gameshark.add(code) + pyboy.tick() + assert pyboy.memory[address] == value + pyboy.gameshark.remove(code, False) + assert pyboy.memory[address] == value + + pyboy.stop(save=False) + + +def test_gameshark_clear(default_rom): + pyboy = PyBoy(default_rom, window="null") + + code1 = "010138CD" + code2 = "010138CE" + value = 0x01 + bank = 0x00 + address1 = 0xCD38 + address2 = 0xCE38 + + assert pyboy.memory[address1] != value + assert pyboy.memory[address2] != value + pyboy.gameshark.add(code1) + pyboy.gameshark.add(code2) + pyboy.tick() + assert pyboy.memory[address1] == value + assert pyboy.memory[address2] == value + + pyboy.gameshark.clear_all() + + pyboy.tick() + assert pyboy.memory[address1] != value + assert pyboy.memory[address2] != value + + pyboy.stop(save=False)