Skip to content

Commit

Permalink
Introduce Gameshark to API
Browse files Browse the repository at this point in the history
Co-authored-by: Artucuno <[email protected]>
Co-authored-by: Mads Ynddal <[email protected]>
  • Loading branch information
3 people committed Sep 13, 2024
1 parent b52a84a commit 8bcb9cd
Show file tree
Hide file tree
Showing 10 changed files with 824 additions and 1 deletion.
522 changes: 522 additions & 0 deletions docs/api/gameshark.html

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/api/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ <h1 class="title">Module <code>pyboy.api</code></h1>
&#34;&#34;&#34;

from . import constants
from .gameshark import GameShark
from .screen import Screen
from .sprite import Sprite
from .tile import Tile
Expand All @@ -54,6 +55,10 @@ <h2 class="section-title" id="header-submodules">Sub-modules</h2>
<dd>
<section class="desc"><p>Memory constants used internally to calculate tile and tile map addresses.</p></section>
</dd>
<dt><code class="name"><a title="pyboy.api.gameshark" href="gameshark.html">pyboy.api.gameshark</a></code></dt>
<dd>
<section class="desc"></section>
</dd>
<dt><code class="name"><a title="pyboy.api.memory_scanner" href="memory_scanner.html">pyboy.api.memory_scanner</a></code></dt>
<dd>
<section class="desc"></section>
Expand Down Expand Up @@ -98,6 +103,7 @@ <h1>Index</h1>
<li><h3><a href="#header-submodules">Sub-modules</a></h3>
<ul>
<li><code><a title="pyboy.api.constants" href="constants.html">pyboy.api.constants</a></code></li>
<li><code><a title="pyboy.api.gameshark" href="gameshark.html">pyboy.api.gameshark</a></code></li>
<li><code><a title="pyboy.api.memory_scanner" href="memory_scanner.html">pyboy.api.memory_scanner</a></code></li>
<li><code><a title="pyboy.api.screen" href="screen.html">pyboy.api.screen</a></code></li>
<li><code><a title="pyboy.api.sprite" href="sprite.html">pyboy.api.sprite</a></code></li>
Expand Down
29 changes: 28 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ <h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="pyboy.PyBoy"><code class="flex name class">
<span>class <span class="ident">PyBoy</span></span>
<span>(</span><span>gamerom, *, window='SDL2', scale=3, symbols=None, bootrom=None, sound=False, sound_emulated=False, cgb=None, log_level='ERROR', **kwargs)</span>
<span>(</span><span>gamerom, *, window='SDL2', scale=3, symbols=None, bootrom=None, sound=False, sound_emulated=False, cgb=None, gameshark=None, log_level='ERROR', **kwargs)</span>
</code></dt>
<dd>
<section class="desc"><p>PyBoy is loadable as an object in Python. This means, it can be initialized from another script, and be
Expand Down Expand Up @@ -121,6 +121,7 @@ <h2 id="kwargs">Kwargs</h2>
sound=False,
sound_emulated=False,
cgb=None,
gameshark=None,
log_level=defaults[&#34;log_level&#34;],
**kwargs
):
Expand Down Expand Up @@ -426,6 +427,21 @@ <h2 id="kwargs">Kwargs</h2>
A game-specific wrapper object.
&#34;&#34;&#34;

self.gameshark = GameShark(self.memory)
&#34;&#34;&#34;
Provides an instance of the `pyboy.api.gameshark.GameShark` handler. This allows you to inject GameShark-based cheat codes.

Example:
```python
&gt;&gt;&gt; pyboy.gameshark.add(&#34;010138CD&#34;)
&gt;&gt;&gt; pyboy.gameshark.remove(&#34;010138CD&#34;)
&gt;&gt;&gt; pyboy.gameshark.clear_all()
```
&#34;&#34;&#34;
if gameshark:
for code in gameshark.split(&#34;,&#34;):
self.gameshark.add(code.strip())

self.initialized = True

def _tick(self, render):
Expand All @@ -436,6 +452,7 @@ <h2 id="kwargs">Kwargs</h2>
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():
Expand Down Expand Up @@ -1490,6 +1507,15 @@ <h2 id="returns">Returns</h2>
<p><code><a title="pyboy.plugins.base_plugin.PyBoyGameWrapper" href="plugins/base_plugin.html#pyboy.plugins.base_plugin.PyBoyGameWrapper">PyBoyGameWrapper</a></code>:
A game-specific wrapper object.</p></section>
</dd>
<dt id="pyboy.PyBoy.gameshark"><code class="name">var <span class="ident">gameshark</span></code></dt>
<dd>
<section class="desc"><p>Provides an instance of the <code><a title="pyboy.api.gameshark.GameShark" href="api/gameshark.html#pyboy.api.gameshark.GameShark">GameShark</a></code> handler. This allows you to inject GameShark-based cheat codes.</p>
<p>Example:</p>
<pre><code class="language-python">&gt;&gt;&gt; pyboy.gameshark.add(&quot;010138CD&quot;)
&gt;&gt;&gt; pyboy.gameshark.remove(&quot;010138CD&quot;)
&gt;&gt;&gt; pyboy.gameshark.clear_all()
</code></pre></section>
</dd>
</dl>
<h3>Methods</h3>
<dl>
Expand Down Expand Up @@ -3604,6 +3630,7 @@ <h4><code><a title="pyboy.PyBoy" href="#pyboy.PyBoy">PyBoy</a></code></h4>
<li><code><a title="pyboy.PyBoy.tilemap_window" href="#pyboy.PyBoy.tilemap_window">tilemap_window</a></code></li>
<li><code><a title="pyboy.PyBoy.cartridge_title" href="#pyboy.PyBoy.cartridge_title">cartridge_title</a></code></li>
<li><code><a title="pyboy.PyBoy.game_wrapper" href="#pyboy.PyBoy.game_wrapper">game_wrapper</a></code></li>
<li><code><a title="pyboy.PyBoy.gameshark" href="#pyboy.PyBoy.gameshark">gameshark</a></code></li>
</ul>
</li>
<li>
Expand Down
5 changes: 5 additions & 0 deletions pyboy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions pyboy/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""

from . import constants
from .gameshark import GameShark
from .screen import Screen
from .sprite import Sprite
from .tile import Tile
Expand Down
24 changes: 24 additions & 0 deletions pyboy/api/gameshark.pxd
Original file line number Diff line number Diff line change
@@ -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
153 changes: 153 additions & 0 deletions pyboy/api/gameshark.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
#

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)
2 changes: 2 additions & 0 deletions pyboy/pyboy.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions pyboy/pyboy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -50,6 +51,7 @@ def __init__(
sound=False,
sound_emulated=False,
cgb=None,
gameshark=None,
log_level=defaults["log_level"],
**kwargs
):
Expand Down Expand Up @@ -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):
Expand All @@ -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():
Expand Down
Loading

0 comments on commit 8bcb9cd

Please sign in to comment.