From c71b3b6d4af2eb38154ee08d6e2447cd268b780b Mon Sep 17 00:00:00 2001 From: Mads Ynddal Date: Sun, 26 May 2024 15:51:31 +0200 Subject: [PATCH] Introduce RTC lock feature --- pyboy/conftest.py | 1 + pyboy/core/cartridge/rtc.pxd | 1 + pyboy/core/cartridge/rtc.py | 10 ++++++++-- pyboy/pyboy.py | 27 +++++++++++++++++++++++++++ tests/test_external_api.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/pyboy/conftest.py b/pyboy/conftest.py index c4cfc661b..35fc59cd4 100644 --- a/pyboy/conftest.py +++ b/pyboy/conftest.py @@ -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) diff --git a/pyboy/core/cartridge/rtc.pxd b/pyboy/core/cartridge/rtc.pxd index 02e5e356a..db7dcd6ea 100644 --- a/pyboy/core/cartridge/rtc.pxd +++ b/pyboy/core/cartridge/rtc.pxd @@ -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 diff --git a/pyboy/core/cartridge/rtc.py b/pyboy/core/cartridge/rtc.py index 5d0a630f1..172edd9f9 100644 --- a/pyboy/core/cartridge/rtc.py +++ b/pyboy/core/cartridge/rtc.py @@ -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) @@ -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 diff --git a/pyboy/pyboy.py b/pyboy/pyboy.py index 63157eaf8..42f196be3 100644 --- a/pyboy/pyboy.py +++ b/pyboy/pyboy.py @@ -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: """ diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 18b9459c7..0d91420d7 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -29,8 +29,15 @@ 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 @@ -79,6 +86,30 @@ def test_rtc_lock(pokemon_gold_rom): 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")