Skip to content

Commit

Permalink
Basic E2E test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
hanzi committed Jan 14, 2025
1 parent 57a57a4 commit e4981e6
Show file tree
Hide file tree
Showing 13 changed files with 495 additions and 34 deletions.
1 change: 1 addition & 0 deletions modules/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(self, initial_bot_mode: str = "Manual"):
self.profile: Optional["Profile"] = None
self.stats: Optional["StatsDatabase"] = None
self.debug: bool = False
self.testing: bool = False

self._current_message: str = ""

Expand Down
9 changes: 5 additions & 4 deletions modules/encounter.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,11 @@ def handle_encounter(
battle_is_active = get_game_state() in (GameState.BATTLE, GameState.BATTLE_STARTING, GameState.BATTLE_ENDING)

if is_of_interest:
filename_suffix = (
f"{encounter_info.value.name}_{make_string_safe_for_file_name(pokemon.species_name_for_stats)}"
)
context.emulator.create_save_state(suffix=filename_suffix)
if not context.testing:
filename_suffix = (
f"{encounter_info.value.name}_{make_string_safe_for_file_name(pokemon.species_name_for_stats)}"
)
context.emulator.create_save_state(suffix=filename_suffix)

if context.config.battle.auto_catch and not disable_auto_catch and battle_is_active:
encounter_info.battle_action = BattleAction.Catch
Expand Down
18 changes: 10 additions & 8 deletions modules/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,19 @@ def _load_event_flags_and_vars(file_name: str) -> None: # TODO Japanese ROMs no

_event_flags.clear()
_reverse_event_flags.clear()
for s in open(get_data_path() / "event_flags" / file_name):
number, name = s.strip().split(" ")
_event_flags[name] = (int(number) // 8) + flags_offset, int(number) % 8
_reverse_event_flags[int(number)] = name
with open(get_data_path() / "event_flags" / file_name) as file_handle:
for s in file_handle:
number, name = s.strip().split(" ")
_event_flags[name] = (int(number) // 8) + flags_offset, int(number) % 8
_reverse_event_flags[int(number)] = name

_event_vars.clear()
_reverse_event_vars.clear()
for s in open(get_data_path() / "event_vars" / file_name):
number, name = s.strip().split(" ")
_event_vars[name] = int(number) * 2 + vars_offset
_reverse_event_vars[int(number)] = name
with open(get_data_path() / "event_vars" / file_name) as file_handle:
for s in file_handle:
number, name = s.strip().split(" ")
_event_vars[name] = int(number) * 2 + vars_offset
_reverse_event_vars[int(number)] = name


def _prepare_character_tables() -> None:
Expand Down
30 changes: 22 additions & 8 deletions modules/libmgba.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ class LibmgbaEmulator:
_audio_sample_rate: int = 32768
_last_audio_data: Queue[bytes]

def __init__(self, profile: Profile, on_frame_callback: callable):
console.print(f"Running [cyan]{libmgba_version_string()}[/]")
def __init__(self, profile: Profile, on_frame_callback: callable, is_test_run: bool = False):
if not is_test_run:
console.print(f"Running [cyan]{libmgba_version_string()}[/]")

# Prevents relentless spamming to stdout by libmgba.
mgba.log.silence()
Expand All @@ -121,12 +122,12 @@ def __init__(self, profile: Profile, on_frame_callback: callable):
# libmgba needs a save file to be loaded, or otherwise it will not save anything
# to disk if the player saves the game. This can be an empty file.
self._current_save_path = profile.path / "current_save.sav"
if not self._current_save_path.exists():
if not is_test_run and not self._current_save_path.exists():
# Create an empty file if a save game does not exist yet.
with open(self._current_save_path, "wb"):
pass
self._save = mgba.vfs.open_path(str(self._current_save_path), "r+")
self._core.load_save(self._save)
self._save = mgba.vfs.open_path(str(self._current_save_path), "r+")
self._core.load_save(self._save)
self._last_audio_data = Queue(maxsize=128)

self._screen = mgba.image.Image(*self._core.desired_video_dimensions())
Expand All @@ -136,7 +137,7 @@ def __init__(self, profile: Profile, on_frame_callback: callable):
# Whenever the emulator closes, it stores the current state in `current_state.ss1`.
# Load this file if it exists, to continue exactly where we left off.
self._current_state_path = profile.path / "current_state.ss1"
if self._current_state_path.exists():
if not is_test_run and self._current_state_path.exists():
with open(self._current_state_path, "rb") as state_file:
self.load_save_state(state_file.read())

Expand All @@ -151,8 +152,9 @@ def __init__(self, profile: Profile, on_frame_callback: callable):
self._pressed_inputs: int = 0
self._held_inputs: int = 0

atexit.register(self.shutdown)
self._core._callbacks.savedata_updated.append(self.backup_current_save_game)
if not is_test_run:
atexit.register(self.shutdown)
self._core._callbacks.savedata_updated.append(self.backup_current_save_game)

def _reset_audio(self) -> None:
"""
Expand Down Expand Up @@ -368,6 +370,18 @@ def load_save_state(self, state: bytes) -> None:
vfile.seek(0, whence=0)
self._core.load_state(vfile)

def load_save_game(self, save_game: bytes) -> None:
"""
Loads GBA save data from a string. This should only be used for testing
because it means that save data will not be written out to a file after
an in-game save.
:param save_game: The raw save game data.
"""
vfile = mgba.vfs.VFile.fromEmpty()
vfile.write(save_game, len(save_game))
vfile.seek(0, whence=0)
self._core.load_save(vfile)

def read_save_data(self) -> bytes:
"""
Reads and returns the contents of the save game (SRAM/Flash)
Expand Down
32 changes: 18 additions & 14 deletions modules/save_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,26 @@ class MigrationError(Exception):


def migrate_save_state(file: IO, profile_name: str, selected_rom: ROM) -> Profile:
selected_rom, state_data, savegame_data = guess_rom_from_save_state(file, selected_rom)

profile = create_profile(profile_name, selected_rom)
if state_data is not None:
with open(profile.path / "current_state.ss1", "wb") as state_file:
state_file.write(state_data)

if savegame_data is not None:
with open(profile.path / "current_save.sav", "wb") as save_file:
save_file.write(savegame_data)

file.close()

return profile


def guess_rom_from_save_state(file, selected_rom) -> tuple[ROM, bytes, bytes | None]:
file.seek(0)
magic = file.read(4)
file.seek(0)

# mGBA state files can either contain the raw serialised state data, or it can
# contain a PNG file that contains a custom 'gbAs' chunk, which in turn contains
# the actual (zlib-compressed) state data. We'd like to support both.
Expand Down Expand Up @@ -58,19 +74,7 @@ def migrate_save_state(file: IO, profile_name: str, selected_rom: ROM) -> Profil
'Please place your .gba ROMs in the "roms/" folder.'
)
selected_rom = matching_rom

profile = create_profile(profile_name, selected_rom)
if state_data is not None:
with open(profile.path / "current_state.ss1", "wb") as state_file:
state_file.write(state_data)

if savegame_data is not None:
with open(profile.path / "current_save.sav", "wb") as save_file:
save_file.write(savegame_data)

file.close()

return profile
return selected_rom, state_data, savegame_data


def get_state_data_from_mgba_state_file(file: IO) -> tuple[bytes, bytes | None]:
Expand Down
Loading

0 comments on commit e4981e6

Please sign in to comment.