diff --git a/__init__.py b/__init__.py index 862af7111125..950b4030ce52 100644 --- a/__init__.py +++ b/__init__.py @@ -8,14 +8,13 @@ from worlds.AutoWorld import WebWorld, World from .client import WL4Client -from .data import data_path -from .items import WL4Item, ap_id_from_wl4_data, filter_item_names, filter_items, item_table -from .locations import get_level_locations, location_name_to_id, location_table, event_table +from .data import Passage, data_path +from .items import ItemType, WL4Item, ap_id_from_wl4_data, filter_item_names, filter_items, item_table +from .locations import LocationType, get_level_locations, location_name_to_id, location_table, event_table from .options import Difficulty, Goal, GoldenJewels, PoolJewels, WL4Options, wl4_option_groups from .regions import connect_regions, create_regions from .rom import MD5_JP, MD5_US_EU, WL4ProcedurePatch, write_tokens from .rules import set_access_rules -from .types import ItemType, LocationType, Passage class WL4Settings(settings.Group): diff --git a/client.py b/client.py index 43f29b05c77e..75ff5e46e25c 100644 --- a/client.py +++ b/client.py @@ -7,12 +7,9 @@ import Utils from NetUtils import ClientStatus import worlds._bizhawk as bizhawk -from worlds._bizhawk import RequestFailedError -from worlds._bizhawk.client import BizHawkClient -from .data import encode_str, get_symbol +from .data import Passage, encode_str, get_symbol from .locations import get_level_locations, location_name_to_id, location_table -from .types import Passage if TYPE_CHECKING: from worlds._bizhawk.context import BizHawkClientContext @@ -140,7 +137,7 @@ def __str__(self): return repr(self) -class WL4Client(BizHawkClient): +class WL4Client(bizhawk.client.BizHawkClient): game = 'Wario Land 4' system = 'GBA' patch_suffix = '.apwl4' @@ -171,7 +168,7 @@ async def validate_rom(self, client_ctx: BizHawkClientContext) -> bool: read(get_symbol('PlayerName'), 64), read(get_symbol('SeedName'), 64), ])) - except RequestFailedError: + except bizhawk.RequestFailedError: return False # Should verify on the next pass game_name = next(read_result).decode('ascii') @@ -271,7 +268,7 @@ async def game_watcher(self, client_ctx: BizHawkClientContext) -> None: read8(fhi_1_address), read8(fhi_2_address), ])) - except RequestFailedError: + except bizhawk.RequestFailedError: return game_mode = next_int(read_result) @@ -426,7 +423,7 @@ async def game_watcher(self, client_ctx: BizHawkClientContext) -> None: try: await bizhawk.guarded_write(bizhawk_ctx, write_list, guard_list) - except RequestFailedError: + except bizhawk.RequestFailedError: return def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: diff --git a/data.py b/data.py index 9f20fca33cc7..2cf096b5d27f 100644 --- a/data.py +++ b/data.py @@ -1,6 +1,6 @@ from __future__ import annotations -from enum import Enum +from enum import Enum, IntEnum, IntFlag from io import StringIO import pkgutil from typing import Mapping @@ -9,6 +9,38 @@ ap_id_offset = 0xEC0000 +class Passage(IntEnum): + ENTRY = 0 + EMERALD = 1 + RUBY = 2 + TOPAZ = 3 + SAPPHIRE = 4 + GOLDEN = 5 + + def long_name(self): + if self == Passage.GOLDEN: + return 'Golden Pyramid' + else: + return self.short_name() + ' Passage' + + def short_name(self): + return ('Entry', 'Emerald', 'Ruby', 'Topaz', 'Sapphire', 'Golden')[self] + + +class ItemFlag(IntFlag): + JEWEL_NE = 1 << 0 + JEWEL_SE = 1 << 1 + JEWEL_SW = 1 << 2 + JEWEL_NW = 1 << 3 + CD = 1 << 4 + KEYZER = 1 << 5 + FULL_HEALTH = 1 << 6 + FULL_HEALTH_2 = 1 << 7 + + BOSS_CLEAR = 1 << 5 + DIVA_CLEAR = 1 << 4 + + class Domain(Enum): SYSTEM_BUS = 0x0000000 ROM = 0x8000000 diff --git a/items.py b/items.py index 73eae75149db..d37114f3e489 100644 --- a/items.py +++ b/items.py @@ -1,12 +1,11 @@ from __future__ import annotations +from enum import IntEnum from typing import Any, Iterable, NamedTuple, Optional, Tuple -from BaseClasses import Item -from BaseClasses import ItemClassification as IC +from BaseClasses import Item, ItemClassification as IC -from .data import ap_id_offset -from .types import Box, ItemFlag, ItemType, Passage +from .data import ap_id_offset, ItemFlag, Passage # Items are encoded as 8-bit numbers as follows: @@ -45,6 +44,23 @@ # For AP item, classifications are as reported by ItemClassification.as_flag() +class Box(IntEnum): + JEWEL_NE = 0 + JEWEL_SE = 1 + JEWEL_SW = 2 + JEWEL_NW = 3 + CD = 4 + FULL_HEALTH = 5 + + +class ItemType(IntEnum): + JEWEL = 0 + CD = 1 + ITEM = 2 + ABILITY = 4 + TREASURE = 5 + + def ap_id_from_wl4_data(data: ItemData) -> int: cat, itemid, _ = data if cat == ItemType.JEWEL: diff --git a/locations.py b/locations.py index c64c98a7cf35..c611ed7bdd2e 100644 --- a/locations.py +++ b/locations.py @@ -1,10 +1,15 @@ +from enum import IntEnum from typing import NamedTuple, Optional, Sequence, Tuple from BaseClasses import Location, Region -from .data import ap_id_offset +from .data import ItemFlag, Passage, ap_id_offset from .options import Difficulty -from .types import ItemFlag, LocationType, Passage + + +class LocationType(IntEnum): + BOX = 0 + CHEST = 3 class LocationData(NamedTuple): diff --git a/regions.py b/regions.py index 2adb4e469668..0c24a79fb4b3 100644 --- a/regions.py +++ b/regions.py @@ -1,13 +1,13 @@ from __future__ import annotations import itertools -from typing import Iterable, Sequence, Set, TYPE_CHECKING +from typing import Callable, Iterable, Optional, Sequence, Set, TYPE_CHECKING -from BaseClasses import Region, Entrance +from BaseClasses import CollectionState, Region, Entrance -from . import rules +from .data import Passage from .locations import WL4Location, get_level_location_data -from .types import AccessRule, Passage +from .rules import get_access_rule, get_frog_switch_region, get_keyzer_region, make_boss_access_rule from .options import Goal, OpenDoors if TYPE_CHECKING: @@ -24,6 +24,9 @@ def pairwise(iterable): return zip(a, b) +AccessRule = Optional[Callable[[CollectionState], bool]] + + class WL4Region(Region): clear_rule: AccessRule @@ -132,7 +135,7 @@ def level_regions(name: str, passage: Passage, level: int): regions = {region: basic_region(region) for region in region_names} for loc_name, location in get_level_location_data(passage, level): if not portal_setting: - region_name = rules.get_frog_switch_region(name) + region_name = get_frog_switch_region(name) elif location.region_in_level is not None: region_name = f'{name} - {location.region_in_level}' else: @@ -194,7 +197,7 @@ def connect_regions(world: WL4World): def connect_level(level_name): regions = get_region_names(level_name) for source, dest in pairwise(regions): - access_rule = rules.get_access_rule(world, dest) + access_rule = get_access_rule(world, dest) connect_entrance(world, dest, source, dest, access_rule) connect_level('Hall of Hieroglyphs') @@ -230,7 +233,7 @@ def connect_level_exit(level, destination, rule: AccessRule = None): ): region = f'{level} (entrance)' elif portal_setting: - region = rules.get_keyzer_region(level) + region = get_keyzer_region(level) else: region = get_region_names(level)[-1] connect_with_name(region, destination, f'{level} Gate', rule) @@ -251,7 +254,7 @@ def connect_level_exit(level, destination, rule: AccessRule = None): connect_level_exit('Mystic Lake', 'Monsoon Jungle (entrance)') connect_level_exit('Monsoon Jungle', 'Emerald Minigame Shop') connect('Emerald Minigame Shop', 'Emerald Passage Boss', - rules.make_boss_access_rule(world, Passage.EMERALD, required_jewels)) + make_boss_access_rule(world, Passage.EMERALD, required_jewels)) connect('Menu', 'Ruby Passage') connect('Ruby Passage', 'The Curious Factory (entrance)') @@ -260,7 +263,7 @@ def connect_level_exit(level, destination, rule: AccessRule = None): connect_level_exit('40 Below Fridge', 'Pinball Zone (entrance)') connect_level_exit('Pinball Zone', 'Ruby Minigame Shop') connect('Ruby Minigame Shop', 'Ruby Passage Boss', - rules.make_boss_access_rule(world, Passage.RUBY, required_jewels)) + make_boss_access_rule(world, Passage.RUBY, required_jewels)) connect('Menu', 'Topaz Passage') connect('Topaz Passage', 'Toy Block Tower (entrance)') @@ -269,7 +272,7 @@ def connect_level_exit(level, destination, rule: AccessRule = None): connect_level_exit('Doodle Woods', 'Domino Row (entrance)') connect_level_exit('Domino Row', 'Topaz Minigame Shop') connect('Topaz Minigame Shop', 'Topaz Passage Boss', - rules.make_boss_access_rule(world, Passage.TOPAZ, required_jewels)) + make_boss_access_rule(world, Passage.TOPAZ, required_jewels)) connect('Menu', 'Sapphire Passage') connect('Sapphire Passage', 'Crescent Moon Village (entrance)') @@ -278,7 +281,7 @@ def connect_level_exit(level, destination, rule: AccessRule = None): connect_level_exit('Fiery Cavern', 'Hotel Horror (entrance)') connect_level_exit('Hotel Horror', 'Sapphire Minigame Shop') connect('Sapphire Minigame Shop', 'Sapphire Passage Boss', - rules.make_boss_access_rule(world, Passage.SAPPHIRE, required_jewels)) + make_boss_access_rule(world, Passage.SAPPHIRE, required_jewels)) connect('Menu', 'Golden Pyramid', lambda state: state.has_all({'Emerald Passage Clear', 'Ruby Passage Clear', @@ -287,7 +290,7 @@ def connect_level_exit(level, destination, rule: AccessRule = None): if world.options.goal != Goal.option_golden_treasure_hunt: connect_level_exit('Golden Passage', 'Golden Minigame Shop') connect('Golden Minigame Shop', 'Golden Pyramid Boss', - rules.make_boss_access_rule(world, Passage.GOLDEN, required_jewels_entry)) + make_boss_access_rule(world, Passage.GOLDEN, required_jewels_entry)) connect('Menu', 'Sound Room') diff --git a/rom.py b/rom.py index 499c277ade84..07aa70aba869 100644 --- a/rom.py +++ b/rom.py @@ -9,9 +9,8 @@ import Utils from worlds.Files import APPatchExtension, APProcedurePatch, APTokenMixin, APTokenTypes -from .data import ap_id_offset, encode_str, get_symbol -from .items import WL4Item, filter_items -from .types import ItemType, Passage +from .data import Passage, ap_id_offset, encode_str, get_symbol +from .items import ItemType, WL4Item, filter_items from .options import Difficulty, Goal, MusicShuffle, OpenDoors, Portal, SmashThroughHardBlocks if TYPE_CHECKING: diff --git a/rules.py b/rules.py index 1d96ba058a0a..0f4603fb9180 100644 --- a/rules.py +++ b/rules.py @@ -5,9 +5,10 @@ from BaseClasses import CollectionState -from . import items, locations, options -from .types import ItemType, Passage -from .options import Goal +from .data import Passage +from .items import ItemType, filter_item_names +from .locations import location_table, event_table +from .options import Difficulty, Goal, Logic if TYPE_CHECKING: from . import WL4World @@ -55,7 +56,7 @@ def has_any(items: Sequence[RequiredItem]) -> Requirement: def has_treasures() -> Requirement: return Requirement(lambda w, s: sum(has(item).inner(w, s) - for item in items.filter_item_names(type=ItemType.TREASURE)) + for item in filter_item_names(type=ItemType.TREASURE)) >= w.options.golden_treasure_count) @@ -98,7 +99,7 @@ def get_access_rule(world: WL4World, region_name: str): def make_boss_access_rule(world: WL4World, passage: Passage, jewels_needed: int): jewel_list = [(name, jewels_needed) - for name in items.filter_item_names(type=ItemType.JEWEL, passage=passage)] + for name in filter_item_names(type=ItemType.JEWEL, passage=passage)] return has_all(jewel_list).apply_world(world) @@ -108,7 +109,7 @@ def set_access_rules(world: WL4World): location = world.get_location(name) location.access_rule = rule.apply_world(world) except KeyError: - assert name in locations.location_table or name in locations.event_table, \ + assert name in location_table or name in event_table, \ f"{name} is not a valid location name" @@ -149,11 +150,11 @@ def set_access_rules(world: WL4World): } -normal = options.Difficulty.option_normal -hard = options.Difficulty.option_hard -s_hard = options.Difficulty.option_s_hard -basic = options.Logic.option_basic -advanced = options.Logic.option_advanced +normal = Difficulty.option_normal +hard = Difficulty.option_hard +s_hard = Difficulty.option_s_hard +basic = Logic.option_basic +advanced = Logic.option_advanced # Regions are linear, so each region from the same level adds to the previous diff --git a/test/test_helpers.py b/test/test_helpers.py index 36f0051765cc..9df21b880c31 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -1,7 +1,9 @@ from . import WL4TestBase -from .. import items, locations -from ..types import ItemType, Passage +from ..data import Passage +from ..items import ItemType, filter_items, filter_item_names +from ..locations import get_level_locations + main_levels = ['Palm Tree Paradise', 'Wildflower Fields', 'Mystic Lake', 'Monsoon Jungle', 'The Curious Factory', 'The Toxic Landfill', '40 Below Fridge', 'Pinball Zone', @@ -13,32 +15,32 @@ class TestHelpers(WL4TestBase): def test_item_filter(self): """Ensure item filters and item names match.""" with self.subTest('Jewel Pieces'): - pieces = items.filter_items(type=ItemType.JEWEL) + pieces = filter_items(type=ItemType.JEWEL) assert all(map(lambda p: p[0].endswith('Piece'), pieces)) assert all(map(lambda p: p[1].type == ItemType.JEWEL, pieces)) with self.subTest('CDs'): - cds = items.filter_item_names(type=ItemType.CD) + cds = filter_item_names(type=ItemType.CD) assert all(map(lambda c: c.endswith('CD'), cds)) for passage in Passage: with self.subTest(passage.long_name()): - pieces = items.filter_item_names(type=ItemType.JEWEL, passage=passage) + pieces = filter_item_names(type=ItemType.JEWEL, passage=passage) assert all(map(lambda p: passage.short_name() in p, pieces)) def test_location_filter(self): """Test that the location filter and location names match""" with self.subTest('Hall of Hieroglyphs'): - checks = locations.get_level_locations(Passage.ENTRY, 0) + checks = get_level_locations(Passage.ENTRY, 0) assert all(map(lambda l: l.startswith('Hall of Hieroglyphs'), checks)) for passage in range(1, 5): for level in range(4): level_name = main_levels[passage * 4 - 4 + level] with self.subTest(level_name): - checks = locations.get_level_locations(Passage(passage), level) + checks = get_level_locations(Passage(passage), level) assert all(map(lambda l: l.startswith(level_name), checks)) with self.subTest('Golden Passage'): - checks = locations.get_level_locations(Passage.GOLDEN, 0) + checks = get_level_locations(Passage.GOLDEN, 0) assert all(map(lambda l: l.startswith('Golden Passage'), checks)) diff --git a/types.py b/types.py deleted file mode 100644 index 46b5dba91f1a..000000000000 --- a/types.py +++ /dev/null @@ -1,61 +0,0 @@ -from enum import IntEnum, IntFlag -from typing import Callable, Optional - -from BaseClasses import CollectionState - - -class ItemType(IntEnum): - JEWEL = 0 - CD = 1 - ITEM = 2 - ABILITY = 4 - TREASURE = 5 - - -class Box(IntEnum): - JEWEL_NE = 0 - JEWEL_SE = 1 - JEWEL_SW = 2 - JEWEL_NW = 3 - CD = 4 - FULL_HEALTH = 5 - - -class LocationType(IntEnum): - BOX = 0 - CHEST = 3 - - -class ItemFlag(IntFlag): - JEWEL_NE = 1 << 0 - JEWEL_SE = 1 << 1 - JEWEL_SW = 1 << 2 - JEWEL_NW = 1 << 3 - CD = 1 << 4 - KEYZER = 1 << 5 - FULL_HEALTH = 1 << 6 - FULL_HEALTH_2 = 1 << 7 - - BOSS_CLEAR = 1 << 5 - DIVA_CLEAR = 1 << 4 - - -class Passage(IntEnum): - ENTRY = 0 - EMERALD = 1 - RUBY = 2 - TOPAZ = 3 - SAPPHIRE = 4 - GOLDEN = 5 - - def long_name(self): - if self == Passage.GOLDEN: - return 'Golden Pyramid' - else: - return self.short_name() + ' Passage' - - def short_name(self): - return ('Entry', 'Emerald', 'Ruby', 'Topaz', 'Sapphire', 'Golden')[self] - - -AccessRule = Optional[Callable[[CollectionState], bool]]