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

RTC fixes and lock feature #329

Merged
merged 5 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
82 changes: 81 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,34 @@ <h2 id="kwargs">Kwargs</h2>
`pyboy.api.tile.Tile`:
A Tile object for the given identifier.
&#34;&#34;&#34;
return Tile(self.mb, identifier=identifier)</code></pre>
return Tile(self.mb, identifier=identifier)

def rtc_lock_experimental(self, enable):
&#34;&#34;&#34;
**WARN: This is an experimental API and is subject to change.**

Lock the Real Time Clock (RTC) of a supporting cartridge. It might be advantageous to lock the RTC when training
an AI in games that use it to change behavior (i.e. day and night).

The first time the game is turned on, an `.rtc` file is created with the current time. This is the epoch for the
RTC. When using `rtc_lock_experimental`, the RTC will always report this point in time. If you let the game progress first,
before using `rtc_lock_experimental`, the internal clock will move backwards and might corrupt the game.

Example:
```python
&gt;&gt;&gt; pyboy = PyBoy(&#39;game_rom.gb&#39;)
&gt;&gt;&gt; pyboy.rtc_lock_experimental(True) # RTC will not progress
```

**WARN: This is an experimental API and is subject to change.**

Args:
enable (float): Point in time to lock RTC to
&#34;&#34;&#34;
if self.mb.cartridge.rtc_enabled:
self.mb.cartridge.rtc.timelock = enable
else:
raise Exception(&#34;There&#39;s no RTC for this cartridge type&#34;)</code></pre>
</details>
<h3>Instance variables</h3>
<dl>
Expand Down Expand Up @@ -2739,6 +2766,58 @@ <h2 id="returns">Returns</h2>
return Tile(self.mb, identifier=identifier)</code></pre>
</details>
</dd>
<dt id="pyboy.PyBoy.rtc_lock_experimental"><code class="name flex">
<span>def <span class="ident">rtc_lock_experimental</span></span>(<span>self, enable)</span>
</code></dt>
<dd>
<section class="desc"><p><strong>WARN: This is an experimental API and is subject to change.</strong></p>
<p>Lock the Real Time Clock (RTC) of a supporting cartridge. It might be advantageous to lock the RTC when training
an AI in games that use it to change behavior (i.e. day and night).</p>
<p>The first time the game is turned on, an <code>.rtc</code> file is created with the current time. This is the epoch for the
RTC. When using <code>rtc_lock_experimental</code>, the RTC will always report this point in time. If you let the game progress first,
before using <code>rtc_lock_experimental</code>, the internal clock will move backwards and might corrupt the game.</p>
<p>Example:</p>
<pre><code class="language-python">&gt;&gt;&gt; pyboy = PyBoy('game_rom.gb')
&gt;&gt;&gt; pyboy.rtc_lock_experimental(True) # RTC will not progress
</code></pre>
<p><strong>WARN: This is an experimental API and is subject to change.</strong></p>
<h2 id="args">Args</h2>
<dl>
<dt><strong><code>enable</code></strong> :&ensp;<code>float</code></dt>
<dd>Point in time to lock RTC to</dd>
</dl></section>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def rtc_lock_experimental(self, enable):
&#34;&#34;&#34;
**WARN: This is an experimental API and is subject to change.**

Lock the Real Time Clock (RTC) of a supporting cartridge. It might be advantageous to lock the RTC when training
an AI in games that use it to change behavior (i.e. day and night).

The first time the game is turned on, an `.rtc` file is created with the current time. This is the epoch for the
RTC. When using `rtc_lock_experimental`, the RTC will always report this point in time. If you let the game progress first,
before using `rtc_lock_experimental`, the internal clock will move backwards and might corrupt the game.

Example:
```python
&gt;&gt;&gt; pyboy = PyBoy(&#39;game_rom.gb&#39;)
&gt;&gt;&gt; pyboy.rtc_lock_experimental(True) # RTC will not progress
```

**WARN: This is an experimental API and is subject to change.**

Args:
enable (float): Point in time to lock RTC to
&#34;&#34;&#34;
if self.mb.cartridge.rtc_enabled:
self.mb.cartridge.rtc.timelock = enable
else:
raise Exception(&#34;There&#39;s no RTC for this cartridge type&#34;)</code></pre>
</details>
</dd>
</dl>
</dd>
<dt id="pyboy.PyBoyMemoryView"><code class="flex name class">
Expand Down Expand Up @@ -3251,6 +3330,7 @@ <h4><code><a title="pyboy.PyBoy" href="#pyboy.PyBoy">PyBoy</a></code></h4>
<li><code><a title="pyboy.PyBoy.get_sprite" href="#pyboy.PyBoy.get_sprite">get_sprite</a></code></li>
<li><code><a title="pyboy.PyBoy.get_sprite_by_tile_identifier" href="#pyboy.PyBoy.get_sprite_by_tile_identifier">get_sprite_by_tile_identifier</a></code></li>
<li><code><a title="pyboy.PyBoy.get_tile" href="#pyboy.PyBoy.get_tile">get_tile</a></code></li>
<li><code><a title="pyboy.PyBoy.rtc_lock_experimental" href="#pyboy.PyBoy.rtc_lock_experimental">rtc_lock_experimental</a></code></li>
<li><code><a title="pyboy.PyBoy.screen" href="#pyboy.PyBoy.screen">screen</a></code></li>
<li><code><a title="pyboy.PyBoy.memory" href="#pyboy.PyBoy.memory">memory</a></code></li>
<li><code><a title="pyboy.PyBoy.memory_scanner" href="#pyboy.PyBoy.memory_scanner">memory_scanner</a></code></li>
Expand Down
1 change: 1 addition & 0 deletions pyboy/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def mock_PyBoy(filename, *args, **kwargs):
mock.patch("pyboy.PyBoy.game_area", return_value=tetris_game_area), \
mock.patch("pyboy.PyBoy.load_state", return_value=None), \
mock.patch("pyboy.PyBoy.stop", return_value=None), \
mock.patch("pyboy.PyBoy.rtc_lock_experimental", return_value=None), \
mock.patch("PIL.Image.Image.show", return_value=None):

pyboy.set_emulation_speed(0)
Expand Down
6 changes: 5 additions & 1 deletion pyboy/core/cartridge/mbc3.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ def setitem(self, address, value):
value = 1
self.rombank_selected = value % self.external_rom_count
elif 0x4000 <= address < 0x6000:
self.rambank_selected = value % self.external_ram_count
if 0x08 <= value <= 0x0C:
# RTC register select
self.rambank_selected = value
else:
self.rambank_selected = value % self.external_ram_count
elif 0x6000 <= address < 0x8000:
if self.rtc_enabled:
self.rtc.writecommand(value)
Expand Down
1 change: 1 addition & 0 deletions pyboy/core/cartridge/rtc.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ cdef class RTC:
cdef str filename
cdef bint latch_enabled
cdef cython.double timezero
cdef bint timelock
cdef uint64_t sec_latch
cdef uint64_t min_latch
cdef uint64_t hour_latch
Expand Down
20 changes: 13 additions & 7 deletions pyboy/core/cartridge/rtc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ class RTC:
def __init__(self, filename):
self.filename = filename + ".rtc"

self.timezero = time.time()
self.timelock = False
self.day_carry = 0
self.halt = 0

if not os.path.exists(self.filename):
logger.info("No RTC file found. Skipping.")
else:
with open(self.filename, "rb") as f:
self.load_state(IntIOWrapper(f), STATE_VERSION)

self.latch_enabled = False

self.timezero = time.time()

self.sec_latch = 0
self.min_latch = 0
self.hour_latch = 0
self.day_latch_low = 0
self.day_latch_high = 0
self.day_carry = 0
self.halt = 0

def stop(self):
with open(self.filename, "wb") as f:
Expand All @@ -51,7 +51,10 @@ def load_state(self, f, state_version):
self.day_carry = f.read()

def latch_rtc(self):
t = time.time() - self.timezero
if self.timelock:
t = 0
else:
t = time.time() - self.timezero
self.sec_latch = int(t % 60)
self.min_latch = int((t//60) % 60)
self.hour_latch = int((t//3600) % 24)
Expand Down Expand Up @@ -99,7 +102,10 @@ def setregister(self, register, value):
if not self.latch_enabled:
logger.debug("RTC: Set register, but nothing is latched! 0x%0.4x, 0x%0.2x", register, value)

t = time.time() - self.timezero
if self.timelock:
t = 0
else:
t = time.time() - self.timezero
if register == 0x08:
# TODO: What happens, when these value are larger than allowed?
self.timezero = self.timezero - (t%60) - value
Expand Down
27 changes: 27 additions & 0 deletions pyboy/pyboy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,33 @@ def get_tile(self, identifier):
"""
return Tile(self.mb, identifier=identifier)

def rtc_lock_experimental(self, enable):
"""
**WARN: This is an experimental API and is subject to change.**

Lock the Real Time Clock (RTC) of a supporting cartridge. It might be advantageous to lock the RTC when training
an AI in games that use it to change behavior (i.e. day and night).

The first time the game is turned on, an `.rtc` file is created with the current time. This is the epoch for the
RTC. When using `rtc_lock_experimental`, the RTC will always report this point in time. If you let the game progress first,
before using `rtc_lock_experimental`, the internal clock will move backwards and might corrupt the game.

Example:
```python
>>> pyboy = PyBoy('game_rom.gb')
>>> pyboy.rtc_lock_experimental(True) # RTC will not progress
```

**WARN: This is an experimental API and is subject to change.**

Args:
enable (float): Point in time to lock RTC to
"""
if self.mb.cartridge.rtc_enabled:
self.mb.cartridge.rtc.timelock = enable
else:
raise Exception("There's no RTC for this cartridge type")


class PyBoyMemoryView:
"""
Expand Down
84 changes: 83 additions & 1 deletion tests/test_external_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import hashlib
import io
import os
import time

import numpy as np
import PIL
Expand All @@ -29,6 +29,88 @@ def test_misc(default_rom):
pyboy.stop(save=False)


def test_rtc_lock_no_rtc(default_rom):
pyboy = PyBoy(default_rom, window="null")
with pytest.raises(Exception):
pyboy.rtc_lock_experimental(True)


def test_rtc_lock(pokemon_gold_rom):
pyboy = PyBoy(pokemon_gold_rom, window="null")
pyboy.rtc_lock_experimental(False)

#Enable external RAM
pyboy.memory[0x0000] = 0x0A

# Reset seconds
pyboy.memory[0x4000] = 0x08
pyboy.memory[0xA000] = 0

# Reset minutes
pyboy.memory[0x4000] = 0x09
pyboy.memory[0xA000] = 0

# Reset hours
pyboy.memory[0x4000] = 0x0a
pyboy.memory[0xA000] = 0

# Reset days (low)
pyboy.memory[0x4000] = 0x0b
pyboy.memory[0xA000] = 0

# Reset days (high)
pyboy.memory[0x4000] = 0x0c
pyboy.memory[0xA000] = 0

time.sleep(2) # Induce a change in the seconds register

# Pan docs:
# When writing $00, and then $01 to this register, the current time becomes latched into the RTC registers
pyboy.memory[0x6000] = 0
pyboy.memory[0x6000] = 1

# Pan docs:
# When writing a value of $08-$0C, this will map the corresponding RTC register into memory at A000-BFFF
pyboy.memory[0x4000] = 0x08
assert pyboy.memory[0xA000] != 0

pyboy.memory[0x4000] = 0x09
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x0a
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x0b
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x0c
assert pyboy.memory[0xA000] == 0

pyboy.rtc_lock_experimental(True)

# Pan docs:
# When writing $00, and then $01 to this register, the current time becomes latched into the RTC registers
pyboy.memory[0x6000] = 0
pyboy.memory[0x6000] = 1

# Pan docs:
# When writing a value of $08-$0C, this will map the corresponding RTC register into memory at A000-BFFF
pyboy.memory[0x4000] = 0x08
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x09
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x0a
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x0b
assert pyboy.memory[0xA000] == 0

pyboy.memory[0x4000] = 0x0c
assert pyboy.memory[0xA000] == 0


def test_faulty_state(default_rom):
pyboy = PyBoy(default_rom, window="null")

Expand Down
Loading