diff --git a/tests/desmume/emulator_utils.py b/tests/desmume/emulator_utils.py index 13b8dcb9..3dcbeee2 100644 --- a/tests/desmume/emulator_utils.py +++ b/tests/desmume/emulator_utils.py @@ -1,14 +1,18 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Generator from contextlib import contextmanager +import logging from pathlib import Path import struct from desmume.emulator import SCREEN_HEIGHT, SCREEN_WIDTH +import pytesseract from ph_rando.patcher._items import ITEMS from ph_rando.shuffler.aux_models import Area +logger = logging.getLogger(__name__) + class AbstractEmulatorWrapper(ABC): video = None @@ -109,6 +113,10 @@ def reset(self): def load_battery_file(self, test_name: str, rom_path: Path): raise NotImplementedError + @abstractmethod + def screenshot(self): + raise NotImplementedError + @property def event_flag_base_addr(self) -> int: addr = int.from_bytes(self.read_memory(start=0x27E0F74, stop=0x27E0F78), 'little') @@ -282,3 +290,23 @@ def cancel_spawn(addr: int, size: int): # Clear callback on context manager exit emu_instance.set_exec_breakpoint(0x20C3FE8, None) + + +def assert_text_displayed(emu_instance: AbstractEmulatorWrapper, text: str) -> None: + """ + Asserts that the given text is displayed on the screen. + """ + from .desmume import DeSmuMEWrapper + + if not isinstance(emu_instance, DeSmuMEWrapper): + logger.warning('Text assertion only supported for DeSmuME emulator.') + return + + screenshot = emu_instance.screenshot() + + # Check if the text is correct + ocr_text: str = pytesseract.image_to_string(screenshot.crop((24, 325, 231, 384))).replace( + '\u2019', "'" + ) + + assert text in ocr_text diff --git a/tests/desmume/melonds.py b/tests/desmume/melonds.py index fb633bdf..6c914fb9 100644 --- a/tests/desmume/melonds.py +++ b/tests/desmume/melonds.py @@ -291,3 +291,6 @@ def reset(self): def stop(self): logger.debug('Stopping MelonDS') self.destroy() + + def screenshot(self): + pytest.skip('MelonDSWrapper does not support screenshots') diff --git a/tests/desmume/test_chest_items.py b/tests/desmume/test_chest_items.py index 478677cc..4051bb55 100644 --- a/tests/desmume/test_chest_items.py +++ b/tests/desmume/test_chest_items.py @@ -2,7 +2,6 @@ from desmume.emulator import SCREEN_WIDTH from ndspy.rom import NintendoDSRom -import pytesseract import pytest from ph_rando.common import ShufflerAuxData @@ -11,7 +10,12 @@ from ph_rando.shuffler.aux_models import Chest, Item from .conftest import GOT_ITEM_TEXT, ITEM_MEMORY_OFFSETS -from .emulator_utils import AbstractEmulatorWrapper, assert_item_is_picked_up, start_first_file +from .emulator_utils import ( + AbstractEmulatorWrapper, + assert_item_is_picked_up, + assert_text_displayed, + start_first_file, +) from .melonds import MelonDSWrapper @@ -71,11 +75,8 @@ def test_custom_chest_items( chest_test_emu.wait(800) # Check if the "got item" text is correct - if hasattr(chest_test_emu, 'screenshot') and item_id in GOT_ITEM_TEXT: - ocr_text: str = pytesseract.image_to_string( - chest_test_emu.screenshot().crop((24, 325, 231, 384)) - ).replace('\u2019', "'") - assert GOT_ITEM_TEXT[item_id] in ocr_text + if item_id in GOT_ITEM_TEXT: + assert_text_displayed(chest_test_emu, GOT_ITEM_TEXT[item_id]) chest_test_emu.touch_set_and_release((0, 0), 2) chest_test_emu.wait(200) diff --git a/tests/desmume/test_data/test_minigame_reward_chests.dsv b/tests/desmume/test_data/test_minigame_reward_chests.dsv new file mode 100644 index 00000000..5598b761 Binary files /dev/null and b/tests/desmume/test_data/test_minigame_reward_chests.dsv differ diff --git a/tests/desmume/test_minigame_reward_chests.py b/tests/desmume/test_minigame_reward_chests.py new file mode 100644 index 00000000..53b8fb37 --- /dev/null +++ b/tests/desmume/test_minigame_reward_chests.py @@ -0,0 +1,218 @@ +from pathlib import Path + +from desmume.emulator import SCREEN_HEIGHT, SCREEN_WIDTH +from ndspy.rom import NintendoDSRom +import pytest + +from ph_rando.common import ShufflerAuxData +from ph_rando.patcher._items import ITEMS_REVERSED +from ph_rando.patcher._util import GD_MODELS, _patch_minigame_items +from ph_rando.shuffler.aux_models import Item, MinigameRewardChest +from tests.desmume.conftest import GOT_ITEM_TEXT, ITEM_MEMORY_OFFSETS +from tests.desmume.emulator_utils import assert_item_is_picked_up + +from .emulator_utils import AbstractEmulatorWrapper, assert_text_displayed, start_first_file +from .melonds import MelonDSWrapper + + +@pytest.fixture +def minigame_reward_chest_emu( + rom_path: Path, + emulator: AbstractEmulatorWrapper, + request, + aux_data: ShufflerAuxData, +): + if isinstance(emulator, MelonDSWrapper): + pytest.skip('MelonDS not supported for this test yet') + + rom = NintendoDSRom.fromFile(rom_path) + chests = [ + chest + for area in aux_data.areas + for room in area.rooms + for chest in room.chests + if type(chest) is MinigameRewardChest + ] + for chest in chests: + chest.contents = Item(name=ITEMS_REVERSED[request.param], states=set()) + + _patch_minigame_items(aux_data.areas, rom) + + rom.saveToFile(rom_path) + + emulator.open(str(rom_path)) + + return emulator + + +@pytest.mark.parametrize( + 'minigame_reward_chest_emu', + [val for val in ITEM_MEMORY_OFFSETS.keys()], + ids=[f'{hex(val)}-{GD_MODELS[val]}' for val in ITEM_MEMORY_OFFSETS.keys()], + indirect=['minigame_reward_chest_emu'], +) +def test_minigame_reward_chests(minigame_reward_chest_emu: AbstractEmulatorWrapper, request): + item_id: int = request.node.callspec.params['minigame_reward_chest_emu'] + + start_first_file(minigame_reward_chest_emu) + + # Skip "arriving in port" cutscene + minigame_reward_chest_emu.touch_set_and_release((SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2), 1) + + # Walk left from boat + minigame_reward_chest_emu.touch_set_and_release((0, SCREEN_HEIGHT // 2), 160) + + # Walk up + minigame_reward_chest_emu.touch_set_and_release((SCREEN_WIDTH // 2, 0), 50) + + # Walk to goron + minigame_reward_chest_emu.touch_set_and_release((0, SCREEN_HEIGHT // 2), 150) # left + minigame_reward_chest_emu.touch_set_and_release((0, SCREEN_HEIGHT), 20) # left and down + minigame_reward_chest_emu.touch_set_and_release((0, SCREEN_HEIGHT // 2), 20) # left + minigame_reward_chest_emu.touch_set_and_release((SCREEN_WIDTH // 2, 0), 5) # up + minigame_reward_chest_emu.wait(20) + + # Talk to goron + minigame_reward_chest_emu.touch_set_and_release((165, 65), 5) # touch goron + minigame_reward_chest_emu.wait(165) + minigame_reward_chest_emu.touch_set_and_release( + (SCREEN_HEIGHT, SCREEN_WIDTH // 2), 5 + ) # advance dialog + minigame_reward_chest_emu.wait(130) + minigame_reward_chest_emu.touch_set_and_release((206, 90), 5) # click "Yes" to play game + + # advance dialog + for _ in range(6): + minigame_reward_chest_emu.wait(150) + minigame_reward_chest_emu.touch_set_and_release((SCREEN_HEIGHT, SCREEN_WIDTH // 2), 5) + + minigame_reward_chest_emu.wait(20) + minigame_reward_chest_emu.touch_set_and_release((206, 90), 5) # click "Yes" to play game + + # advance dialog + for _ in range(11): + minigame_reward_chest_emu.wait(150) + minigame_reward_chest_emu.touch_set_and_release((SCREEN_HEIGHT, SCREEN_WIDTH // 2), 5) + + minigame_reward_chest_emu.wait(300) + + # Start rolling + for x in range(40): + minigame_reward_chest_emu.touch_set((SCREEN_WIDTH // 2) - x * 2, SCREEN_HEIGHT // 2) + minigame_reward_chest_emu.wait(1) + + minigame_reward_chest_emu.wait(115) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(80) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(30) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + minigame_reward_chest_emu.wait(40) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(20) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(60) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(30) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(35) + + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(30) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(10) + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(50) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(80) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(190) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + minigame_reward_chest_emu.wait(40) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(20) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + minigame_reward_chest_emu.wait(70) + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(70) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(80) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + minigame_reward_chest_emu.wait(55) + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(40) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + + minigame_reward_chest_emu.wait(23) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2 + 10) # Right + minigame_reward_chest_emu.wait(45) + + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + minigame_reward_chest_emu.wait(40) + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(35) + + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(50) + + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(60) + minigame_reward_chest_emu.touch_release() + + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(100) + + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(40) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + + # # Wait for door to open + minigame_reward_chest_emu.wait(260) + + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(50) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, SCREEN_HEIGHT) # Down + minigame_reward_chest_emu.wait(50) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH, SCREEN_HEIGHT // 2) # Right + minigame_reward_chest_emu.wait(60) + minigame_reward_chest_emu.touch_set(SCREEN_WIDTH // 2, 0) # Up + minigame_reward_chest_emu.wait(40) + minigame_reward_chest_emu.touch_set(0, SCREEN_HEIGHT // 2) # Left + minigame_reward_chest_emu.wait(200) + minigame_reward_chest_emu.touch_release() # End of race + + minigame_reward_chest_emu.wait(80) + minigame_reward_chest_emu.touch_set_and_release( + (SCREEN_HEIGHT, SCREEN_WIDTH // 2), 5 + ) # advance dialog + + minigame_reward_chest_emu.wait(200) + minigame_reward_chest_emu.touch_set_and_release( + (SCREEN_HEIGHT, SCREEN_WIDTH // 2), 5 + ) # advance dialog + minigame_reward_chest_emu.wait(150) + minigame_reward_chest_emu.touch_set_and_release( + (SCREEN_HEIGHT, SCREEN_WIDTH // 2), 5 + ) # advance dialog + minigame_reward_chest_emu.wait(50) + + minigame_reward_chest_emu.touch_set_and_release((SCREEN_WIDTH // 2, SCREEN_HEIGHT), 15) # Down + minigame_reward_chest_emu.touch_set_and_release((SCREEN_WIDTH, SCREEN_HEIGHT // 2), 30) # Right + + with assert_item_is_picked_up(item_id, minigame_reward_chest_emu): + minigame_reward_chest_emu.wait(10) + minigame_reward_chest_emu.touch_set_and_release((107, 67), 2) # Open chest + + # Wait for "Got item" text + minigame_reward_chest_emu.wait(800) + + # Check if the "got item" text is correct + if item_id in GOT_ITEM_TEXT: + assert_text_displayed(minigame_reward_chest_emu, GOT_ITEM_TEXT[item_id]) + + minigame_reward_chest_emu.touch_set_and_release((0, 0), 2) + minigame_reward_chest_emu.wait(200) + minigame_reward_chest_emu.touch_set_and_release((0, 0), 2) + minigame_reward_chest_emu.wait(100) + minigame_reward_chest_emu.touch_set_and_release((0, 0), 2) + minigame_reward_chest_emu.wait(100)