From 58fcd50721dc773c5b90c5ac40ef9b68552f3fb1 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:42:07 -0500 Subject: [PATCH] Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit" This reverts commit a2360fe197e77a723bb70006c5eb5725c7ed3826, reversing changes made to b8948bc4958855c6e342e18bdb8dc81cfcf09455. --- BaseClasses.py | 218 +++++++---- Fill.py | 45 ++- Generate.py | 17 +- Main.py | 7 +- SNIClient.py | 4 +- Utils.py | 64 +++- WebHostLib/options.py | 6 + WebHostLib/static/assets/faq/faq_en.md | 109 +++--- WebHostLib/static/assets/weighted-options.js | 291 ++++----------- WebHostLib/static/styles/weighted-options.css | 6 + WebHostLib/templates/lttpMultiTracker.html | 2 +- WebHostLib/templates/multiTracker.html | 10 +- WebHostLib/tracker.py | 19 +- test/bases.py | 28 +- test/general/test_fill.py | 4 +- test/general/test_host_yaml.py | 4 +- test/general/test_locations.py | 3 - worlds/AutoWorld.py | 36 +- worlds/__init__.py | 42 ++- worlds/_bizhawk/context.py | 63 +++- worlds/adventure/Rom.py | 6 +- worlds/alttp/Client.py | 3 +- worlds/alttp/Dungeons.py | 3 +- worlds/alttp/ItemPool.py | 1 - worlds/alttp/Rom.py | 6 +- worlds/alttp/Rules.py | 28 +- worlds/alttp/Shops.py | 3 - worlds/alttp/UnderworldGlitchRules.py | 2 +- worlds/alttp/__init__.py | 46 +-- worlds/alttp/test/dungeons/TestDungeon.py | 2 +- worlds/archipidle/Rules.py | 7 +- worlds/blasphemous/Options.py | 1 + worlds/blasphemous/Rules.py | 8 +- worlds/blasphemous/docs/en_Blasphemous.md | 1 + worlds/checksfinder/__init__.py | 4 +- worlds/checksfinder/docs/en_ChecksFinder.md | 13 +- worlds/dlcquest/Rules.py | 4 +- worlds/dlcquest/__init__.py | 4 +- worlds/ff1/docs/en_Final Fantasy.md | 3 +- worlds/hk/Items.py | 45 ++- worlds/hk/Rules.py | 15 +- worlds/hk/__init__.py | 16 +- worlds/ladx/Locations.py | 6 +- worlds/ladx/__init__.py | 13 +- worlds/lufia2ac/Rom.py | 5 +- worlds/meritous/Regions.py | 4 +- worlds/messenger/__init__.py | 2 +- worlds/minecraft/__init__.py | 2 +- .../mmbn3/docs/en_MegaMan Battle Network 3.md | 7 + worlds/musedash/MuseDashData.txt | 11 +- worlds/musedash/__init__.py | 2 +- worlds/noita/Items.py | 84 +++-- worlds/noita/Regions.py | 74 ++-- worlds/noita/Rules.py | 13 +- worlds/oot/Entrance.py | 10 +- worlds/oot/EntranceShuffle.py | 52 ++- worlds/oot/Patches.py | 2 +- worlds/oot/Rules.py | 21 +- worlds/oot/__init__.py | 344 ++++++++++-------- worlds/oot/docs/en_Ocarina of Time.md | 7 + worlds/pokemon_rb/__init__.py | 17 +- worlds/pokemon_rb/basepatch_blue.bsdiff4 | Bin 45570 -> 45893 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45511 -> 45875 bytes .../docs/en_Pokemon Red and Blue.md | 6 + worlds/pokemon_rb/rom.py | 6 +- worlds/pokemon_rb/rom_addresses.py | 10 +- worlds/pokemon_rb/rules.py | 38 +- worlds/ror2/Options.py | 42 +-- worlds/ror2/Rules.py | 3 +- .../docs/en_Starcraft 2 Wings of Liberty.md | 22 +- worlds/sm/__init__.py | 19 +- worlds/sm64ex/Options.py | 7 + worlds/sm64ex/Rules.py | 7 +- worlds/sm64ex/__init__.py | 1 + worlds/smz3/__init__.py | 8 +- worlds/soe/__init__.py | 2 +- worlds/stardew_valley/mods/mod_data.py | 8 + worlds/stardew_valley/stardew_rule.py | 16 +- worlds/stardew_valley/test/TestBackpack.py | 49 +-- worlds/stardew_valley/test/TestGeneration.py | 39 +- worlds/stardew_valley/test/TestItems.py | 6 +- .../test/TestLogicSimplification.py | 91 ++--- worlds/stardew_valley/test/TestOptions.py | 35 +- worlds/stardew_valley/test/TestRegions.py | 4 +- worlds/stardew_valley/test/TestRules.py | 2 +- worlds/stardew_valley/test/__init__.py | 137 +++---- .../test/checks/world_checks.py | 10 +- .../stardew_valley/test/long/TestModsLong.py | 16 +- .../test/long/TestOptionsLong.py | 7 +- .../test/long/TestRandomWorlds.py | 8 +- .../test/mods/TestBiggerBackpack.py | 51 ++- worlds/stardew_valley/test/mods/TestMods.py | 25 +- worlds/terraria/docs/setup_en.md | 2 + worlds/tloz/docs/en_The Legend of Zelda.md | 14 +- worlds/tloz/docs/multiworld_en.md | 1 + worlds/undertale/__init__.py | 2 +- worlds/undertale/docs/en_Undertale.md | 19 +- worlds/wargroove/docs/en_Wargroove.md | 9 +- worlds/witness/hints.py | 4 +- worlds/zillion/__init__.py | 33 +- worlds/zillion/docs/en_Zillion.md | 10 +- 101 files changed, 1458 insertions(+), 1186 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index d35739c324..a70dd70a92 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,14 +1,15 @@ from __future__ import annotations import copy +import itertools import functools import logging import random import secrets import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace -from collections import ChainMap, Counter, deque -from collections.abc import Collection +from collections import Counter, deque +from collections.abc import Collection, MutableSequence from enum import IntEnum, IntFlag from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \ Type, ClassVar @@ -47,7 +48,6 @@ class ThreadBarrierProxy: class MultiWorld(): debug_types = False player_name: Dict[int, str] - _region_cache: Dict[int, Dict[str, Region]] difficulty_requirements: dict required_medallions: dict dark_room_logic: Dict[int, str] @@ -57,7 +57,7 @@ class MultiWorld(): plando_connections: List worlds: Dict[int, auto_world] groups: Dict[int, Group] - regions: List[Region] + regions: RegionManager itempool: List[Item] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -92,6 +92,34 @@ class MultiWorld(): def __getitem__(self, player) -> bool: return self.rule(player) + class RegionManager: + region_cache: Dict[int, Dict[str, Region]] + entrance_cache: Dict[int, Dict[str, Entrance]] + location_cache: Dict[int, Dict[str, Location]] + + def __init__(self, players: int): + self.region_cache = {player: {} for player in range(1, players+1)} + self.entrance_cache = {player: {} for player in range(1, players+1)} + self.location_cache = {player: {} for player in range(1, players+1)} + + def __iadd__(self, other: Iterable[Region]): + self.extend(other) + return self + + def append(self, region: Region): + self.region_cache[region.player][region.name] = region + + def extend(self, regions: Iterable[Region]): + for region in regions: + self.region_cache[region.player][region.name] = region + + def __iter__(self) -> Iterator[Region]: + for regions in self.region_cache.values(): + yield from regions.values() + + def __len__(self): + return sum(len(regions) for regions in self.region_cache.values()) + def __init__(self, players: int): # world-local random state is saved for multiple generations running concurrently self.random = ThreadBarrierProxy(random.Random()) @@ -100,16 +128,12 @@ class MultiWorld(): self.glitch_triforce = False self.algorithm = 'balanced' self.groups = {} - self.regions = [] + self.regions = self.RegionManager(players) self.shops = [] self.itempool = [] self.seed = None self.seed_name: str = "Unavailable" self.precollected_items = {player: [] for player in self.player_ids} - self._cached_entrances = None - self._cached_locations = None - self._entrance_cache = {} - self._location_cache: Dict[Tuple[str, int], Location] = {} self.required_locations = [] self.light_world_light_cone = False self.dark_world_light_cone = False @@ -137,7 +161,6 @@ class MultiWorld(): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val - set_player_attr('_region_cache', {}) set_player_attr('shuffle', "vanilla") set_player_attr('logic', "noglitches") set_player_attr('mode', 'open') @@ -199,7 +222,6 @@ class MultiWorld(): self.game[new_id] = game self.player_types[new_id] = NetUtils.SlotType.group - self._region_cache[new_id] = {} world_type = AutoWorld.AutoWorldRegister.world_types[game] self.worlds[new_id] = world_type.create_group(self, new_id, players) self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) @@ -303,11 +325,15 @@ class MultiWorld(): def player_ids(self) -> Tuple[int, ...]: return tuple(range(1, self.players + 1)) - @functools.lru_cache() + @Utils.cache_self1 def get_game_players(self, game_name: str) -> Tuple[int, ...]: return tuple(player for player in self.player_ids if self.game[player] == game_name) - @functools.lru_cache() + @Utils.cache_self1 + def get_game_groups(self, game_name: str) -> Tuple[int, ...]: + return tuple(group_id for group_id in self.groups if self.game[group_id] == game_name) + + @Utils.cache_self1 def get_game_worlds(self, game_name: str): return tuple(world for player, world in self.worlds.items() if player not in self.groups and self.game[player] == game_name) @@ -329,41 +355,17 @@ class MultiWorld(): def world_name_lookup(self): return {self.player_name[player_id]: player_id for player_id in self.player_ids} - def _recache(self): - """Rebuild world cache""" - self._cached_locations = None - for region in self.regions: - player = region.player - self._region_cache[player][region.name] = region - for exit in region.exits: - self._entrance_cache[exit.name, player] = exit - - for r_location in region.locations: - self._location_cache[r_location.name, player] = r_location - def get_regions(self, player: Optional[int] = None) -> Collection[Region]: - return self.regions if player is None else self._region_cache[player].values() + return self.regions if player is None else self.regions.region_cache[player].values() - def get_region(self, regionname: str, player: int) -> Region: - try: - return self._region_cache[player][regionname] - except KeyError: - self._recache() - return self._region_cache[player][regionname] + def get_region(self, region_name: str, player: int) -> Region: + return self.regions.region_cache[player][region_name] - def get_entrance(self, entrance: str, player: int) -> Entrance: - try: - return self._entrance_cache[entrance, player] - except KeyError: - self._recache() - return self._entrance_cache[entrance, player] + def get_entrance(self, entrance_name: str, player: int) -> Entrance: + return self.regions.entrance_cache[player][entrance_name] - def get_location(self, location: str, player: int) -> Location: - try: - return self._location_cache[location, player] - except KeyError: - self._recache() - return self._location_cache[location, player] + def get_location(self, location_name: str, player: int) -> Location: + return self.regions.location_cache[player][location_name] def get_all_state(self, use_cache: bool) -> CollectionState: cached = getattr(self, "_all_state", None) @@ -424,28 +426,22 @@ class MultiWorld(): logging.debug('Placed %s at %s', item, location) - def get_entrances(self) -> List[Entrance]: - if self._cached_entrances is None: - self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances] - return self._cached_entrances - - def clear_entrance_cache(self): - self._cached_entrances = None + def get_entrances(self, player: Optional[int] = None) -> Iterable[Entrance]: + if player is not None: + return self.regions.entrance_cache[player].values() + return Utils.RepeatableChain(tuple(self.regions.entrance_cache[player].values() + for player in self.regions.entrance_cache)) def register_indirect_condition(self, region: Region, entrance: Entrance): """Report that access to this Region can result in unlocking this Entrance, state.can_reach(Region) in the Entrance's traversal condition, as opposed to pure transition logic.""" self.indirect_connections.setdefault(region, set()).add(entrance) - def get_locations(self, player: Optional[int] = None) -> List[Location]: - if self._cached_locations is None: - self._cached_locations = [location for region in self.regions for location in region.locations] + def get_locations(self, player: Optional[int] = None) -> Iterable[Location]: if player is not None: - return [location for location in self._cached_locations if location.player == player] - return self._cached_locations - - def clear_location_cache(self): - self._cached_locations = None + return self.regions.location_cache[player].values() + return Utils.RepeatableChain(tuple(self.regions.location_cache[player].values() + for player in self.regions.location_cache)) def get_unfilled_locations(self, player: Optional[int] = None) -> List[Location]: return [location for location in self.get_locations(player) if location.item is None] @@ -467,16 +463,17 @@ class MultiWorld(): valid_locations = [location.name for location in self.get_unfilled_locations(player)] else: valid_locations = location_names + relevant_cache = self.regions.location_cache[player] for location_name in valid_locations: - location = self._location_cache.get((location_name, player), None) - if location is not None and location.item is None: + location = relevant_cache.get(location_name, None) + if location and location.item is None: yield location def unlocks_new_location(self, item: Item) -> bool: temp_state = self.state.copy() temp_state.collect(item, True) - for location in self.get_unfilled_locations(): + for location in self.get_unfilled_locations(item.player): if temp_state.can_reach(location) and not self.state.can_reach(location): return True @@ -608,7 +605,7 @@ PathValue = Tuple[str, Optional["PathValue"]] class CollectionState(): - prog_items: typing.Counter[Tuple[str, int]] + prog_items: Dict[int, Counter[str]] multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] @@ -620,7 +617,7 @@ class CollectionState(): additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] def __init__(self, parent: MultiWorld): - self.prog_items = Counter() + self.prog_items = {player: Counter() for player in parent.player_ids} self.multiworld = parent self.reachable_regions = {player: set() for player in parent.get_all_ids()} self.blocked_connections = {player: set() for player in parent.get_all_ids()} @@ -668,7 +665,7 @@ class CollectionState(): def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = self.prog_items.copy() + ret.prog_items = copy.deepcopy(self.prog_items) ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in self.reachable_regions} ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in @@ -712,23 +709,23 @@ class CollectionState(): self.collect(event.item, True, event) def has(self, item: str, player: int, count: int = 1) -> bool: - return self.prog_items[item, player] >= count + return self.prog_items[player][item] >= count def has_all(self, items: Set[str], player: int) -> bool: """Returns True if each item name of items is in state at least once.""" - return all(self.prog_items[item, player] for item in items) + return all(self.prog_items[player][item] for item in items) def has_any(self, items: Set[str], player: int) -> bool: """Returns True if at least one item name of items is in state at least once.""" - return any(self.prog_items[item, player] for item in items) + return any(self.prog_items[player][item] for item in items) def count(self, item: str, player: int) -> int: - return self.prog_items[item, player] + return self.prog_items[player][item] def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool: found: int = 0 for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]: - found += self.prog_items[item_name, player] + found += self.prog_items[player][item_name] if found >= count: return True return False @@ -736,11 +733,11 @@ class CollectionState(): def count_group(self, item_name_group: str, player: int) -> int: found: int = 0 for item_name in self.multiworld.worlds[player].item_name_groups[item_name_group]: - found += self.prog_items[item_name, player] + found += self.prog_items[player][item_name] return found def item_count(self, item: str, player: int) -> int: - return self.prog_items[item, player] + return self.prog_items[player][item] def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: @@ -749,7 +746,7 @@ class CollectionState(): changed = self.multiworld.worlds[item.player].collect(self, item) if not changed and event: - self.prog_items[item.name, item.player] += 1 + self.prog_items[item.player][item.name] += 1 changed = True self.stale[item.player] = True @@ -816,15 +813,83 @@ class Region: locations: List[Location] entrance_type: ClassVar[Type[Entrance]] = Entrance + class Register(MutableSequence): + region_manager: MultiWorld.RegionManager + + def __init__(self, region_manager: MultiWorld.RegionManager): + self._list = [] + self.region_manager = region_manager + + def __getitem__(self, index: int) -> Location: + return self._list.__getitem__(index) + + def __setitem__(self, index: int, value: Location) -> None: + raise NotImplementedError() + + def __len__(self) -> int: + return self._list.__len__() + + # This seems to not be needed, but that's a bit suspicious. + # def __del__(self): + # self.clear() + + def copy(self): + return self._list.copy() + + class LocationRegister(Register): + def __delitem__(self, index: int) -> None: + location: Location = self._list.__getitem__(index) + self._list.__delitem__(index) + del(self.region_manager.location_cache[location.player][location.name]) + + def insert(self, index: int, value: Location) -> None: + self._list.insert(index, value) + self.region_manager.location_cache[value.player][value.name] = value + + class EntranceRegister(Register): + def __delitem__(self, index: int) -> None: + entrance: Entrance = self._list.__getitem__(index) + self._list.__delitem__(index) + del(self.region_manager.entrance_cache[entrance.player][entrance.name]) + + def insert(self, index: int, value: Entrance) -> None: + self._list.insert(index, value) + self.region_manager.entrance_cache[value.player][value.name] = value + + _locations: LocationRegister[Location] + _exits: EntranceRegister[Entrance] + def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None): self.name = name self.entrances = [] - self.exits = [] - self.locations = [] + self._exits = self.EntranceRegister(multiworld.regions) + self._locations = self.LocationRegister(multiworld.regions) self.multiworld = multiworld self._hint_text = hint self.player = player + def get_locations(self): + return self._locations + + def set_locations(self, new): + if new is self._locations: + return + self._locations.clear() + self._locations.extend(new) + + locations = property(get_locations, set_locations) + + def get_exits(self): + return self._exits + + def set_exits(self, new): + if new is self._exits: + return + self._exits.clear() + self._exits.extend(new) + + exits = property(get_exits, set_exits) + def can_reach(self, state: CollectionState) -> bool: if state.stale[self.player]: state.update_reachable_regions(self.player) @@ -855,7 +920,7 @@ class Region: self.locations.append(location_type(self.player, location, address, self)) def connect(self, connecting_region: Region, name: Optional[str] = None, - rule: Optional[Callable[[CollectionState], bool]] = None) -> None: + rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -866,6 +931,7 @@ class Region: if rule: exit_.access_rule = rule exit_.connect(connecting_region) + return exit_ def create_exit(self, name: str) -> Entrance: """ diff --git a/Fill.py b/Fill.py index 9d5dc0b457..c9660ab708 100644 --- a/Fill.py +++ b/Fill.py @@ -15,6 +15,10 @@ class FillError(RuntimeError): pass +def _log_fill_progress(name: str, placed: int, total_items: int) -> None: + logging.info(f"Current fill step ({name}) at {placed}/{total_items} items placed.") + + def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] = tuple()) -> CollectionState: new_state = base_state.copy() for item in itempool: @@ -26,7 +30,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item] def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location], item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False, swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None, - allow_partial: bool = False, allow_excluded: bool = False) -> None: + allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None: """ :param world: Multiworld to be filled. :param base_state: State assumed before fill. @@ -38,16 +42,20 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: :param on_place: callback that is called when a placement happens :param allow_partial: only place what is possible. Remaining items will be in the item_pool list. :param allow_excluded: if true and placement fails, it is re-attempted while ignoring excluded on Locations + :param name: name of this fill step for progress logging purposes """ unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] cleanup_required = False - swapped_items: typing.Counter[typing.Tuple[int, str, bool]] = Counter() reachable_items: typing.Dict[int, typing.Deque[Item]] = {} for item in item_pool: reachable_items.setdefault(item.player, deque()).append(item) + # for progress logging + total = min(len(item_pool), len(locations)) + placed = 0 + while any(reachable_items.values()) and locations: # grab one item per player items_to_place = [items.pop() @@ -152,9 +160,15 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: spot_to_fill.locked = lock placements.append(spot_to_fill) spot_to_fill.event = item_to_place.advancement + placed += 1 + if not placed % 1000: + _log_fill_progress(name, placed, total) if on_place: on_place(spot_to_fill) + if total > 1000: + _log_fill_progress(name, placed, total) + if cleanup_required: # validate all placements and remove invalid ones state = sweep_from_pool(base_state, []) @@ -198,6 +212,8 @@ def remaining_fill(world: MultiWorld, unplaced_items: typing.List[Item] = [] placements: typing.List[Location] = [] swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() + total = min(len(itempool), len(locations)) + placed = 0 while locations and itempool: item_to_place = itempool.pop() spot_to_fill: typing.Optional[Location] = None @@ -247,6 +263,12 @@ def remaining_fill(world: MultiWorld, world.push_item(spot_to_fill, item_to_place, False) placements.append(spot_to_fill) + placed += 1 + if not placed % 1000: + _log_fill_progress("Remaining", placed, total) + + if total > 1000: + _log_fill_progress("Remaining", placed, total) if unplaced_items and locations: # There are leftover unplaceable items and locations that won't accept them @@ -282,7 +304,7 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio locations.append(location) if pool and locations: locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) - fill_restrictive(world, state, locations, pool) + fill_restrictive(world, state, locations, pool, name="Accessibility Corrections") def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): @@ -352,23 +374,25 @@ def distribute_early_items(world: MultiWorld, player_local = early_local_rest_items[player] fill_restrictive(world, base_state, [loc for loc in early_locations if loc.player == player], - player_local, lock=True, allow_partial=True) + player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}") if player_local: logging.warning(f"Could not fulfill rules of early items: {player_local}") early_rest_items.extend(early_local_rest_items[player]) early_locations = [loc for loc in early_locations if not loc.item] - fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True) + fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True, + name="Early Items") early_locations += early_priority_locations for player in world.player_ids: player_local = early_local_prog_items[player] fill_restrictive(world, base_state, [loc for loc in early_locations if loc.player == player], - player_local, lock=True, allow_partial=True) + player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}") if player_local: logging.warning(f"Could not fulfill rules of early items: {player_local}") early_prog_items.extend(player_local) early_locations = [loc for loc in early_locations if not loc.item] - fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True) + fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True, + name="Early Progression") unplaced_early_items = early_rest_items + early_prog_items if unplaced_early_items: logging.warning("Ran out of early locations for early items. Failed to place " @@ -422,13 +446,14 @@ def distribute_items_restrictive(world: MultiWorld) -> None: if prioritylocations: # "priority fill" - fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking, + name="Priority") accessibility_corrections(world, world.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: - # "progression fill" - fill_restrictive(world, world.state, defaultlocations, progitempool) + # "advancement/progression fill" + fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression") if progitempool: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') diff --git a/Generate.py b/Generate.py index 34a0084e8d..8113d8a0d7 100644 --- a/Generate.py +++ b/Generate.py @@ -7,8 +7,8 @@ import random import string import urllib.parse import urllib.request -from collections import ChainMap, Counter -from typing import Any, Callable, Dict, Tuple, Union +from collections import Counter +from typing import Any, Dict, Tuple, Union import ModuleUpdate @@ -225,7 +225,7 @@ def main(args=None, callback=ERmain): with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f: yaml.dump(important, f) - callback(erargs, seed) + return callback(erargs, seed) def read_weights_yamls(path) -> Tuple[Any, ...]: @@ -639,6 +639,15 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): if __name__ == '__main__': import atexit confirmation = atexit.register(input, "Press enter to close.") - main() + multiworld = main() + if __debug__: + import gc + import sys + import weakref + weak = weakref.ref(multiworld) + del multiworld + gc.collect() # need to collect to deref all hard references + assert not weak(), f"MultiWorld object was not de-allocated, it's referenced {sys.getrefcount(weak())} times." \ + " This would be a memory leak." # in case of error-free exit should not need confirmation atexit.unregister(confirmation) diff --git a/Main.py b/Main.py index 0995d2091f..691b88b137 100644 --- a/Main.py +++ b/Main.py @@ -122,10 +122,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger.info('Creating Items.') AutoWorld.call_all(world, "create_items") - # All worlds should have finished creating all regions, locations, and entrances. - # Recache to ensure that they are all visible for locality rules. - world._recache() - logger.info('Calculating Access Rules.') for player in world.player_ids: @@ -233,7 +229,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No region = Region("Menu", group_id, world, "ItemLink") world.regions.append(region) - locations = region.locations = [] + locations = region.locations for item in world.itempool: count = common_item_count.get(item.player, {}).get(item.name, 0) if count: @@ -267,7 +263,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.itempool.extend(items_to_add[:itemcount - len(world.itempool)]) if any(world.item_links.values()): - world._recache() world._all_state = None logger.info("Running Item Plando") diff --git a/SNIClient.py b/SNIClient.py index 0909c61382..062d7a7cbe 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -207,12 +207,12 @@ class SNIContext(CommonContext): self.killing_player_task = asyncio.create_task(deathlink_kill_player(self)) super(SNIContext, self).on_deathlink(data) - async def handle_deathlink_state(self, currently_dead: bool) -> None: + async def handle_deathlink_state(self, currently_dead: bool, death_text: str = "") -> None: # in this state we only care about triggering a death send if self.death_state == DeathState.alive: if currently_dead: self.death_state = DeathState.dead - await self.send_death() + await self.send_death(death_text) # in this state we care about confirming a kill, to move state to dead elif self.death_state == DeathState.killing_player: # this is being handled in deathlink_kill_player(ctx) already diff --git a/Utils.py b/Utils.py index 5fb037a173..bb68602cce 100644 --- a/Utils.py +++ b/Utils.py @@ -5,6 +5,7 @@ import json import typing import builtins import os +import itertools import subprocess import sys import pickle @@ -73,6 +74,8 @@ def snes_to_pc(value: int) -> int: RetType = typing.TypeVar("RetType") +S = typing.TypeVar("S") +T = typing.TypeVar("T") def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[], RetType]: @@ -90,6 +93,31 @@ def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[] return _wrap +def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[S, T], RetType]: + """Specialized cache for self + 1 arg. Does not keep global ref to self and skips building a dict key tuple.""" + + assert function.__code__.co_argcount == 2, "Can only cache 2 argument functions with this cache." + + cache_name = f"__cache_{function.__name__}__" + + @functools.wraps(function) + def wrap(self: S, arg: T) -> RetType: + cache: Optional[Dict[T, RetType]] = typing.cast(Optional[Dict[T, RetType]], + getattr(self, cache_name, None)) + if cache is None: + res = function(self, arg) + setattr(self, cache_name, {arg: res}) + return res + try: + return cache[arg] + except KeyError: + res = function(self, arg) + cache[arg] = res + return res + + return wrap + + def is_frozen() -> bool: return typing.cast(bool, getattr(sys, 'frozen', False)) @@ -146,12 +174,16 @@ def user_path(*path: str) -> str: if user_path.cached_path != local_path(): import filecmp if not os.path.exists(user_path("manifest.json")) or \ + not os.path.exists(local_path("manifest.json")) or \ not filecmp.cmp(local_path("manifest.json"), user_path("manifest.json"), shallow=True): import shutil - for dn in ("Players", "data/sprites"): + for dn in ("Players", "data/sprites", "data/lua"): shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) - for fn in ("manifest.json",): - shutil.copy2(local_path(fn), user_path(fn)) + if not os.path.exists(local_path("manifest.json")): + warnings.warn(f"Upgrading {user_path()} from something that is not a proper install") + else: + shutil.copy2(local_path("manifest.json"), user_path("manifest.json")) + os.makedirs(user_path("worlds"), exist_ok=True) return os.path.join(user_path.cached_path, *path) @@ -257,15 +289,13 @@ def get_public_ipv6() -> str: return ip -OptionsType = Settings # TODO: remove ~2 versions after 0.4.1 +OptionsType = Settings # TODO: remove when removing get_options -@cache_argsless -def get_default_options() -> Settings: # TODO: remove ~2 versions after 0.4.1 - return Settings(None) - - -get_options = get_settings # TODO: add a warning ~2 versions after 0.4.1 and remove once all games are ported +def get_options() -> Settings: + # TODO: switch to Utils.deprecate after 0.4.4 + warnings.warn("Utils.get_options() is deprecated. Use the settings API instead.", DeprecationWarning) + return get_settings() def persistent_store(category: str, key: typing.Any, value: typing.Any): @@ -905,3 +935,17 @@ def visualize_regions(root_region: Region, file_name: str, *, with open(file_name, "wt", encoding="utf-8") as f: f.write("\n".join(uml)) + + +class RepeatableChain: + def __init__(self, iterable: typing.Iterable): + self.iterable = iterable + + def __iter__(self): + return itertools.chain.from_iterable(self.iterable) + + def __bool__(self): + return any(sub_iterable for sub_iterable in self.iterable) + + def __len__(self): + return sum(len(iterable) for iterable in self.iterable) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 785785cde0..1a2aab6d88 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -139,7 +139,13 @@ def create(): weighted_options["games"][game_name] = {} weighted_options["games"][game_name]["gameSettings"] = game_options weighted_options["games"][game_name]["gameItems"] = tuple(world.item_names) + weighted_options["games"][game_name]["gameItemGroups"] = [ + group for group in world.item_name_groups.keys() if group != "Everything" + ] weighted_options["games"][game_name]["gameLocations"] = tuple(world.location_names) + weighted_options["games"][game_name]["gameLocationGroups"] = [ + group for group in world.location_name_groups.keys() if group != "Everywhere" + ] with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: json.dump(weighted_options, f, indent=2, separators=(',', ': ')) diff --git a/WebHostLib/static/assets/faq/faq_en.md b/WebHostLib/static/assets/faq/faq_en.md index 74f423df1f..fb1ccd2d6f 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -2,13 +2,62 @@ ## What is a randomizer? -A randomizer is a modification of a video game which reorganizes the items required to progress through the game. A -normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a randomized +A randomizer is a modification of a game which reorganizes the items required to progress through that game. A +normal play-through might require you to use item A to unlock item B, then C, and so forth. In a randomized game, you might first find item C, then A, then B. -This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they -play a randomized game. Putting items in non-standard locations can require the player to think about the game world and -the items they encounter in new and interesting ways. +This transforms the game from a linear experience into a puzzle, presenting players with a new challenge each time they +play. Putting items in non-standard locations can require the player to think about the game world and the items they +encounter in new and interesting ways. + +## What is a multiworld? + +While a randomizer shuffles a game, a multiworld randomizer shuffles that game for multiple players. For example, in a +two player multiworld, players A and B each get their own randomized version of a game, called a world. In each +player's game, they may find items which belong to the other player. If player A finds an item which belongs to +player B, the item will be sent to player B's world over the internet. This creates a cooperative experience, requiring +players to rely upon each other to complete their game. + +## What does multi-game mean? + +While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows +players to randomize any of the supported games, and send items between them. This allows players of different +games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld. +Here is a list of our [Supported Games](https://archipelago.gg/games). + +## Can I generate a single-player game with Archipelago? + +Yes. All of our supported games can be generated as single-player experiences both on the website and by installing +the Archipelago generator software. The fastest way to do this is on the website. Find the Supported Game you wish to +play, open the Settings Page, pick your settings, and click Generate Game. + +## How do I get started? + +We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the +software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for +including multiple games, and hosting multiworlds on the website for ease and convenience. + +If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join +our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer +any questions you might have. + +## What are some common terms I should know? + +As randomizers and multiworld randomizers have been around for a while now, there are quite a few common terms used +by the communities surrounding them. A list of Archipelago jargon and terms commonly used by the community can be +found in the [Glossary](/glossary/en). + +## Does everyone need to be connected at the same time? + +There are two different play-styles that are common for Archipelago multiworld sessions. These sessions can either +be considered synchronous (or "sync"), where everyone connects and plays at the same time, or asynchronous (or "async"), +where players connect and play at their own pace. The setup for both is identical. The difference in play-style is how +you and your friends choose to organize and play your multiworld. Most groups decide on the format before creating +their multiworld. + +If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items +in that game belonging to other players are sent out automatically. This allows other players to continue to play +uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en). ## What happens if an item is placed somewhere it is impossible to get? @@ -17,53 +66,15 @@ is to ensure items necessary to complete the game will be accessible to the play rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are comfortable exploiting certain glitches in the game. -## What is a multi-world? - -While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a -two player multi-world, players A and B each get their own randomized version of a game, called a world. In each player's -game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the -item will be sent to player B's world over the internet. - -This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete -their game. - -## What happens if a person has to leave early? - -If a player must leave early, they can use Archipelago's release system. When a player releases their game, all the -items in that game which belong to other players are sent out automatically, so other players can continue to play. - -## What does multi-game mean? - -While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world allows -players to randomize any of a number of supported games, and send items between them. This allows players of different -games to interact with one another in a single multiplayer environment. - -## Can I generate a single-player game with Archipelago? - -Yes. All our supported games can be generated as single-player experiences, and so long as you download the software, -the website is not required to generate them. - -## How do I get started? - -If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join -our discord server at the [Archipelago Discord](https://discord.gg/8Z65BR2). There are always people ready to answer -any questions you might have. - -## What are some common terms I should know? - -As randomizers and multiworld randomizers have been around for a while now there are quite a lot of common terms -and jargon that is used in conjunction by the communities surrounding them. For a lot of the terms that are more common -to Archipelago and its specific systems please see the [Glossary](/glossary/en). - ## I want to add a game to the Archipelago randomizer. How do I do that? -The best way to get started is to take a look at our code on GitHub -at [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). +The best way to get started is to take a look at our code on GitHub: +[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). -There you will find examples of games in the worlds folder -at [/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds). +There, you will find examples of games in the `worlds` folder: +[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds). -You may also find developer documentation in the docs folder -at [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). +You may also find developer documentation in the `docs` folder: +[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord. diff --git a/WebHostLib/static/assets/weighted-options.js b/WebHostLib/static/assets/weighted-options.js index bdd121eff5..3811bd42ba 100644 --- a/WebHostLib/static/assets/weighted-options.js +++ b/WebHostLib/static/assets/weighted-options.js @@ -43,7 +43,7 @@ const resetSettings = () => { }; const fetchSettingData = () => new Promise((resolve, reject) => { - fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => { + fetch(new Request(`${window.location.origin}/static/generated/weighted-options.json`)).then((response) => { try{ response.json().then((jsonObj) => resolve(jsonObj)); } catch(error){ reject(error); } }); @@ -428,13 +428,13 @@ class GameSettings { const weightedSettingsDiv = this.#buildWeightedSettingsDiv(); gameDiv.appendChild(weightedSettingsDiv); - const itemPoolDiv = this.#buildItemsDiv(); + const itemPoolDiv = this.#buildItemPoolDiv(); gameDiv.appendChild(itemPoolDiv); const hintsDiv = this.#buildHintsDiv(); gameDiv.appendChild(hintsDiv); - const locationsDiv = this.#buildLocationsDiv(); + const locationsDiv = this.#buildPriorityExclusionDiv(); gameDiv.appendChild(locationsDiv); collapseButton.addEventListener('click', () => { @@ -734,107 +734,17 @@ class GameSettings { break; case 'items-list': - const itemsList = document.createElement('div'); - itemsList.classList.add('simple-list'); - - Object.values(this.data.gameItems).forEach((item) => { - const itemRow = document.createElement('div'); - itemRow.classList.add('list-row'); - - const itemLabel = document.createElement('label'); - itemLabel.setAttribute('for', `${this.name}-${settingName}-${item}`) - - const itemCheckbox = document.createElement('input'); - itemCheckbox.setAttribute('id', `${this.name}-${settingName}-${item}`); - itemCheckbox.setAttribute('type', 'checkbox'); - itemCheckbox.setAttribute('data-game', this.name); - itemCheckbox.setAttribute('data-setting', settingName); - itemCheckbox.setAttribute('data-option', item.toString()); - itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - if (this.current[settingName].includes(item)) { - itemCheckbox.setAttribute('checked', '1'); - } - - const itemName = document.createElement('span'); - itemName.innerText = item.toString(); - - itemLabel.appendChild(itemCheckbox); - itemLabel.appendChild(itemName); - - itemRow.appendChild(itemLabel); - itemsList.appendChild((itemRow)); - }); - + const itemsList = this.#buildItemsDiv(settingName); settingWrapper.appendChild(itemsList); break; case 'locations-list': - const locationsList = document.createElement('div'); - locationsList.classList.add('simple-list'); - - Object.values(this.data.gameLocations).forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-${settingName}-${location}`) - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('id', `${this.name}-${settingName}-${location}`); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', settingName); - locationCheckbox.setAttribute('data-option', location.toString()); - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - if (this.current[settingName].includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - - const locationName = document.createElement('span'); - locationName.innerText = location.toString(); - - locationLabel.appendChild(locationCheckbox); - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - locationsList.appendChild((locationRow)); - }); - + const locationsList = this.#buildLocationsDiv(settingName); settingWrapper.appendChild(locationsList); break; case 'custom-list': - const customList = document.createElement('div'); - customList.classList.add('simple-list'); - - Object.values(this.data.gameSettings[settingName].options).forEach((listItem) => { - const customListRow = document.createElement('div'); - customListRow.classList.add('list-row'); - - const customItemLabel = document.createElement('label'); - customItemLabel.setAttribute('for', `${this.name}-${settingName}-${listItem}`) - - const customItemCheckbox = document.createElement('input'); - customItemCheckbox.setAttribute('id', `${this.name}-${settingName}-${listItem}`); - customItemCheckbox.setAttribute('type', 'checkbox'); - customItemCheckbox.setAttribute('data-game', this.name); - customItemCheckbox.setAttribute('data-setting', settingName); - customItemCheckbox.setAttribute('data-option', listItem.toString()); - customItemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - if (this.current[settingName].includes(listItem)) { - customItemCheckbox.setAttribute('checked', '1'); - } - - const customItemName = document.createElement('span'); - customItemName.innerText = listItem.toString(); - - customItemLabel.appendChild(customItemCheckbox); - customItemLabel.appendChild(customItemName); - - customListRow.appendChild(customItemLabel); - customList.appendChild((customListRow)); - }); - + const customList = this.#buildListDiv(settingName, this.data.gameSettings[settingName].options); settingWrapper.appendChild(customList); break; @@ -849,7 +759,7 @@ class GameSettings { return settingsWrapper; } - #buildItemsDiv() { + #buildItemPoolDiv() { const itemsDiv = document.createElement('div'); itemsDiv.classList.add('items-div'); @@ -1058,35 +968,7 @@ class GameSettings { itemHintsWrapper.classList.add('hints-wrapper'); itemHintsWrapper.innerText = 'Starting Item Hints'; - const itemHintsDiv = document.createElement('div'); - itemHintsDiv.classList.add('simple-list'); - this.data.gameItems.forEach((item) => { - const itemRow = document.createElement('div'); - itemRow.classList.add('list-row'); - - const itemLabel = document.createElement('label'); - itemLabel.setAttribute('for', `${this.name}-start_hints-${item}`); - - const itemCheckbox = document.createElement('input'); - itemCheckbox.setAttribute('type', 'checkbox'); - itemCheckbox.setAttribute('id', `${this.name}-start_hints-${item}`); - itemCheckbox.setAttribute('data-game', this.name); - itemCheckbox.setAttribute('data-setting', 'start_hints'); - itemCheckbox.setAttribute('data-option', item); - if (this.current.start_hints.includes(item)) { - itemCheckbox.setAttribute('checked', 'true'); - } - itemCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - itemLabel.appendChild(itemCheckbox); - - const itemName = document.createElement('span'); - itemName.innerText = item; - itemLabel.appendChild(itemName); - - itemRow.appendChild(itemLabel); - itemHintsDiv.appendChild(itemRow); - }); - + const itemHintsDiv = this.#buildItemsDiv('start_hints'); itemHintsWrapper.appendChild(itemHintsDiv); itemHintsContainer.appendChild(itemHintsWrapper); @@ -1095,35 +977,7 @@ class GameSettings { locationHintsWrapper.classList.add('hints-wrapper'); locationHintsWrapper.innerText = 'Starting Location Hints'; - const locationHintsDiv = document.createElement('div'); - locationHintsDiv.classList.add('simple-list'); - this.data.gameLocations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-start_location_hints-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${this.name}-start_location_hints-${location}`); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', 'start_location_hints'); - locationCheckbox.setAttribute('data-option', location); - if (this.current.start_location_hints.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - locationHintsDiv.appendChild(locationRow); - }); - + const locationHintsDiv = this.#buildLocationsDiv('start_location_hints'); locationHintsWrapper.appendChild(locationHintsDiv); itemHintsContainer.appendChild(locationHintsWrapper); @@ -1131,7 +985,7 @@ class GameSettings { return hintsDiv; } - #buildLocationsDiv() { + #buildPriorityExclusionDiv() { const locationsDiv = document.createElement('div'); locationsDiv.classList.add('locations-div'); const locationsHeader = document.createElement('h3'); @@ -1151,35 +1005,7 @@ class GameSettings { priorityLocationsWrapper.classList.add('locations-wrapper'); priorityLocationsWrapper.innerText = 'Priority Locations'; - const priorityLocationsDiv = document.createElement('div'); - priorityLocationsDiv.classList.add('simple-list'); - this.data.gameLocations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-priority_locations-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${this.name}-priority_locations-${location}`); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', 'priority_locations'); - locationCheckbox.setAttribute('data-option', location); - if (this.current.priority_locations.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - priorityLocationsDiv.appendChild(locationRow); - }); - + const priorityLocationsDiv = this.#buildLocationsDiv('priority_locations'); priorityLocationsWrapper.appendChild(priorityLocationsDiv); locationsContainer.appendChild(priorityLocationsWrapper); @@ -1188,35 +1014,7 @@ class GameSettings { excludeLocationsWrapper.classList.add('locations-wrapper'); excludeLocationsWrapper.innerText = 'Exclude Locations'; - const excludeLocationsDiv = document.createElement('div'); - excludeLocationsDiv.classList.add('simple-list'); - this.data.gameLocations.forEach((location) => { - const locationRow = document.createElement('div'); - locationRow.classList.add('list-row'); - - const locationLabel = document.createElement('label'); - locationLabel.setAttribute('for', `${this.name}-exclude_locations-${location}`); - - const locationCheckbox = document.createElement('input'); - locationCheckbox.setAttribute('type', 'checkbox'); - locationCheckbox.setAttribute('id', `${this.name}-exclude_locations-${location}`); - locationCheckbox.setAttribute('data-game', this.name); - locationCheckbox.setAttribute('data-setting', 'exclude_locations'); - locationCheckbox.setAttribute('data-option', location); - if (this.current.exclude_locations.includes(location)) { - locationCheckbox.setAttribute('checked', '1'); - } - locationCheckbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); - locationLabel.appendChild(locationCheckbox); - - const locationName = document.createElement('span'); - locationName.innerText = location; - locationLabel.appendChild(locationName); - - locationRow.appendChild(locationLabel); - excludeLocationsDiv.appendChild(locationRow); - }); - + const excludeLocationsDiv = this.#buildLocationsDiv('exclude_locations'); excludeLocationsWrapper.appendChild(excludeLocationsDiv); locationsContainer.appendChild(excludeLocationsWrapper); @@ -1224,6 +1022,71 @@ class GameSettings { return locationsDiv; } + // Builds a div for a setting whose value is a list of locations. + #buildLocationsDiv(setting) { + return this.#buildListDiv(setting, this.data.gameLocations, this.data.gameLocationGroups); + } + + // Builds a div for a setting whose value is a list of items. + #buildItemsDiv(setting) { + return this.#buildListDiv(setting, this.data.gameItems, this.data.gameItemGroups); + } + + // Builds a div for a setting named `setting` with a list value that can + // contain `items`. + // + // The `groups` option can be a list of additional options for this list + // (usually `item_name_groups` or `location_name_groups`) that are displayed + // in a special section at the top of the list. + #buildListDiv(setting, items, groups = []) { + const div = document.createElement('div'); + div.classList.add('simple-list'); + + groups.forEach((group) => { + const row = this.#addListRow(setting, group); + div.appendChild(row); + }); + + if (groups.length > 0) { + div.appendChild(document.createElement('hr')); + } + + items.forEach((item) => { + const row = this.#addListRow(setting, item); + div.appendChild(row); + }); + + return div; + } + + // Builds and returns a row for a list of checkboxes. + #addListRow(setting, item) { + const row = document.createElement('div'); + row.classList.add('list-row'); + + const label = document.createElement('label'); + label.setAttribute('for', `${this.name}-${setting}-${item}`); + + const checkbox = document.createElement('input'); + checkbox.setAttribute('type', 'checkbox'); + checkbox.setAttribute('id', `${this.name}-${setting}-${item}`); + checkbox.setAttribute('data-game', this.name); + checkbox.setAttribute('data-setting', setting); + checkbox.setAttribute('data-option', item); + if (this.current[setting].includes(item)) { + checkbox.setAttribute('checked', '1'); + } + checkbox.addEventListener('change', (evt) => this.#updateListSetting(evt)); + label.appendChild(checkbox); + + const name = document.createElement('span'); + name.innerText = item; + label.appendChild(name); + + row.appendChild(label); + return row; + } + #updateRangeSetting(evt) { const setting = evt.target.getAttribute('data-setting'); const option = evt.target.getAttribute('data-option'); diff --git a/WebHostLib/static/styles/weighted-options.css b/WebHostLib/static/styles/weighted-options.css index cc5231634e..8a66ca2370 100644 --- a/WebHostLib/static/styles/weighted-options.css +++ b/WebHostLib/static/styles/weighted-options.css @@ -292,6 +292,12 @@ html{ margin-right: 0.5rem; } +#weighted-settings .simple-list hr{ + width: calc(100% - 2px); + margin: 2px auto; + border-bottom: 1px solid rgb(255 255 255 / 0.6); +} + #weighted-settings .invisible{ display: none; } diff --git a/WebHostLib/templates/lttpMultiTracker.html b/WebHostLib/templates/lttpMultiTracker.html index 2b943a22b0..8eb471be39 100644 --- a/WebHostLib/templates/lttpMultiTracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -153,7 +153,7 @@ {%- endif -%} {% endif %} {%- endfor -%} - {{ percent_total_checks_done[team][player] }} + {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} {%- if activity_timers[(team, player)] -%} {{ activity_timers[(team, player)].total_seconds() }} {%- else -%} diff --git a/WebHostLib/templates/multiTracker.html b/WebHostLib/templates/multiTracker.html index 40d89eb4c6..1a3d353de1 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -55,7 +55,7 @@ {{ checks["Total"] }}/{{ locations[player] | length }} - {{ percent_total_checks_done[team][player] }} + {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} {%- if activity_timers[team, player] -%} {{ activity_timers[team, player].total_seconds() }} {%- else -%} @@ -72,7 +72,13 @@ All Games {{ completed_worlds }}/{{ players|length }} Complete {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} - {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} + + {% if total_locations[team] == 0 %} + 100 + {% else %} + {{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }} + {% endif %} + diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 0d9ead7951..55b98df59e 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1532,9 +1532,11 @@ def _get_multiworld_tracker_data(tracker: UUID) -> typing.Optional[typing.Dict[s continue player_locations = locations[player] checks_done[team][player]["Total"] = len(locations_checked) - percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / - len(player_locations) * 100) \ - if player_locations else 100 + percent_total_checks_done[team][player] = ( + checks_done[team][player]["Total"] / len(player_locations) * 100 + if player_locations + else 100 + ) activity_timers = {} now = datetime.datetime.utcnow() @@ -1690,10 +1692,13 @@ def get_LttP_multiworld_tracker(tracker: UUID): for recipient in recipients: attribute_item(team, recipient, item) checks_done[team][player][player_location_to_area[player][location]] += 1 - checks_done[team][player]["Total"] += 1 - percent_total_checks_done[team][player] = int( - checks_done[team][player]["Total"] / len(player_locations) * 100) if \ - player_locations else 100 + checks_done[team][player]["Total"] = len(locations_checked) + + percent_total_checks_done[team][player] = ( + checks_done[team][player]["Total"] / len(player_locations) * 100 + if player_locations + else 100 + ) for (team, player), game_state in multisave.get("client_game_state", {}).items(): if player in groups: diff --git a/test/bases.py b/test/bases.py index 5fe4df2014..2054c2d187 100644 --- a/test/bases.py +++ b/test/bases.py @@ -1,3 +1,4 @@ +import sys import typing import unittest from argparse import Namespace @@ -107,11 +108,36 @@ class WorldTestBase(unittest.TestCase): game: typing.ClassVar[str] # define game name in subclass, example "Secret of Evermore" auto_construct: typing.ClassVar[bool] = True """ automatically set up a world for each test in this class """ + memory_leak_tested: typing.ClassVar[bool] = False + """ remember if memory leak test was already done for this class """ def setUp(self) -> None: if self.auto_construct: self.world_setup() + def tearDown(self) -> None: + if self.__class__.memory_leak_tested or not self.options or not self.constructed or \ + sys.version_info < (3, 11, 0): # the leak check in tearDown fails in py<3.11 for an unknown reason + # only run memory leak test once per class, only for constructed with non-default options + # default options will be tested in test/general + super().tearDown() + return + + import gc + import weakref + weak = weakref.ref(self.multiworld) + for attr_name in dir(self): # delete all direct references to MultiWorld and World + attr: object = typing.cast(object, getattr(self, attr_name)) + if type(attr) is MultiWorld or isinstance(attr, AutoWorld.World): + delattr(self, attr_name) + state_cache: typing.Optional[typing.Dict[typing.Any, typing.Any]] = getattr(self, "_state_cache", None) + if state_cache is not None: # in case of multiple inheritance with TestBase, we need to clear its cache + state_cache.clear() + gc.collect() + self.__class__.memory_leak_tested = True + self.assertFalse(weak(), f"World {getattr(self, 'game', '')} leaked MultiWorld object") + super().tearDown() + def world_setup(self, seed: typing.Optional[int] = None) -> None: if type(self) is WorldTestBase or \ (hasattr(WorldTestBase, self._testMethodName) @@ -284,7 +310,7 @@ class WorldTestBase(unittest.TestCase): # basically a shortened reimplementation of this method from core, in order to force the check is done def fulfills_accessibility() -> bool: - locations = self.multiworld.get_locations(1).copy() + locations = list(self.multiworld.get_locations(1)) state = CollectionState(self.multiworld) while locations: sphere: typing.List[Location] = [] diff --git a/test/general/test_fill.py b/test/general/test_fill.py index 4e8cc2edb7..1e469ef04d 100644 --- a/test/general/test_fill.py +++ b/test/general/test_fill.py @@ -455,8 +455,8 @@ class TestFillRestrictive(unittest.TestCase): location.place_locked_item(item) multi_world.state.sweep_for_events() multi_world.state.sweep_for_events() - self.assertTrue(multi_world.state.prog_items[item.name, item.player], "Sweep did not collect - Test flawed") - self.assertEqual(multi_world.state.prog_items[item.name, item.player], 1, "Sweep collected multiple times") + self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed") + self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times") def test_correct_item_instance_removed_from_pool(self): """Test that a placed item gets removed from the submitted pool""" diff --git a/test/general/test_host_yaml.py b/test/general/test_host_yaml.py index 9408f95b16..79285d3a63 100644 --- a/test/general/test_host_yaml.py +++ b/test/general/test_host_yaml.py @@ -16,7 +16,7 @@ class TestIDs(unittest.TestCase): def test_utils_in_yaml(self) -> None: """Tests that the auto generated host.yaml has default settings in it""" - for option_key, option_set in Utils.get_default_options().items(): + for option_key, option_set in Settings(None).items(): with self.subTest(option_key): self.assertIn(option_key, self.yaml_options) for sub_option_key in option_set: @@ -24,7 +24,7 @@ class TestIDs(unittest.TestCase): def test_yaml_in_utils(self) -> None: """Tests that the auto generated host.yaml shows up in reference calls""" - utils_options = Utils.get_default_options() + utils_options = Settings(None) for option_key, option_set in self.yaml_options.items(): with self.subTest(option_key): self.assertIn(option_key, utils_options) diff --git a/test/general/test_locations.py b/test/general/test_locations.py index 2e609a756f..63b3b0f364 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -36,7 +36,6 @@ class TestBase(unittest.TestCase): for game_name, world_type in AutoWorldRegister.world_types.items(): with self.subTest("Game", game_name=game_name): multiworld = setup_solo_multiworld(world_type, gen_steps) - multiworld._recache() region_count = len(multiworld.get_regions()) location_count = len(multiworld.get_locations()) @@ -46,14 +45,12 @@ class TestBase(unittest.TestCase): self.assertEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during rule creation") - multiworld._recache() call_all(multiworld, "generate_basic") self.assertEqual(region_count, len(multiworld.get_regions()), f"{game_name} modified region count during generate_basic") self.assertGreaterEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during generate_basic") - multiworld._recache() call_all(multiworld, "pre_fill") self.assertEqual(region_count, len(multiworld.get_regions()), f"{game_name} modified region count during pre_fill") diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index d4fe0f49a2..d05797cf9e 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -4,6 +4,7 @@ import hashlib import logging import pathlib import sys +import time from dataclasses import make_dataclass from typing import Any, Callable, ClassVar, Dict, Set, Tuple, FrozenSet, List, Optional, TYPE_CHECKING, TextIO, Type, \ Union @@ -17,6 +18,8 @@ if TYPE_CHECKING: from . import GamesPackage from settings import Group +perf_logger = logging.getLogger("performance") + class AutoWorldRegister(type): world_types: Dict[str, Type[World]] = {} @@ -103,10 +106,24 @@ class AutoLogicRegister(type): return new_class +def _timed_call(method: Callable[..., Any], *args: Any, + multiworld: Optional["MultiWorld"] = None, player: Optional[int] = None) -> Any: + start = time.perf_counter() + ret = method(*args) + taken = time.perf_counter() - start + if taken > 1.0: + if player and multiworld: + perf_logger.info(f"Took {taken} seconds in {method.__qualname__} for player {player}, " + f"named {multiworld.player_name[player]}.") + else: + perf_logger.info(f"Took {taken} seconds in {method.__qualname__}.") + return ret + + def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any: method = getattr(multiworld.worlds[player], method_name) try: - ret = method(*args) + ret = _timed_call(method, *args, multiworld=multiworld, player=player) except Exception as e: message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}." if sys.version_info >= (3, 11, 0): @@ -132,18 +149,15 @@ def call_all(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: f"Duplicate item reference of \"{item.name}\" in \"{multiworld.worlds[player].game}\" " f"of player \"{multiworld.player_name[player]}\". Please make a copy instead.") - for world_type in sorted(world_types, key=lambda world: world.__name__): - stage_callable = getattr(world_type, f"stage_{method_name}", None) - if stage_callable: - stage_callable(multiworld, *args) + call_stage(multiworld, method_name, *args) def call_stage(multiworld: "MultiWorld", method_name: str, *args: Any) -> None: world_types = {multiworld.worlds[player].__class__ for player in multiworld.player_ids} - for world_type in world_types: + for world_type in sorted(world_types, key=lambda world: world.__name__): stage_callable = getattr(world_type, f"stage_{method_name}", None) if stage_callable: - stage_callable(multiworld, *args) + _timed_call(stage_callable, multiworld, *args) class WebWorld: @@ -400,16 +414,16 @@ class World(metaclass=AutoWorldRegister): def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: - state.prog_items[name, self.player] += 1 + state.prog_items[self.player][name] += 1 return True return False def remove(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item, True) if name: - state.prog_items[name, self.player] -= 1 - if state.prog_items[name, self.player] < 1: - del (state.prog_items[name, self.player]) + state.prog_items[self.player][name] -= 1 + if state.prog_items[self.player][name] < 1: + del (state.prog_items[self.player][name]) return True return False diff --git a/worlds/__init__.py b/worlds/__init__.py index c6208fa9a1..40e0b20f19 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -5,19 +5,20 @@ import typing import warnings import zipimport -folder = os.path.dirname(__file__) +from Utils import user_path, local_path -__all__ = { +local_folder = os.path.dirname(__file__) +user_folder = user_path("worlds") if user_path() != local_path() else None + +__all__ = ( "lookup_any_item_id_to_name", "lookup_any_location_id_to_name", "network_data_package", "AutoWorldRegister", "world_sources", - "folder", -} - -if typing.TYPE_CHECKING: - from .AutoWorld import World + "local_folder", + "user_folder", +) class GamesData(typing.TypedDict): @@ -41,13 +42,13 @@ class WorldSource(typing.NamedTuple): is_zip: bool = False relative: bool = True # relative to regular world import folder - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})" @property def resolved_path(self) -> str: if self.relative: - return os.path.join(folder, self.path) + return os.path.join(local_folder, self.path) return self.path def load(self) -> bool: @@ -56,6 +57,7 @@ class WorldSource(typing.NamedTuple): importer = zipimport.zipimporter(self.resolved_path) if hasattr(importer, "find_spec"): # new in Python 3.10 spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) + assert spec, f"{self.path} is not a loadable module" mod = importlib.util.module_from_spec(spec) else: # TODO: remove with 3.8 support mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) @@ -72,7 +74,7 @@ class WorldSource(typing.NamedTuple): importlib.import_module(f".{self.path}", "worlds") return True - except Exception as e: + except Exception: # A single world failing can still mean enough is working for the user, log and carry on import traceback import io @@ -87,14 +89,16 @@ class WorldSource(typing.NamedTuple): # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.List[WorldSource] = [] -file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly -for file in os.scandir(folder): - # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "." - if not file.name.startswith(("_", ".")): - if file.is_dir(): - world_sources.append(WorldSource(file.name)) - elif file.is_file() and file.name.endswith(".apworld"): - world_sources.append(WorldSource(file.name, is_zip=True)) +for folder in (folder for folder in (user_folder, local_folder) if folder): + relative = folder == local_folder + for entry in os.scandir(folder): + # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "." + if not entry.name.startswith(("_", ".")): + file_name = entry.name if relative else os.path.join(folder, entry.name) + if entry.is_dir(): + world_sources.append(WorldSource(file_name, relative=relative)) + elif entry.is_file() and entry.name.endswith(".apworld"): + world_sources.append(WorldSource(file_name, is_zip=True, relative=relative)) # import all submodules to trigger AutoWorldRegister world_sources.sort() @@ -105,7 +109,7 @@ lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} games: typing.Dict[str, GamesPackage] = {} -from .AutoWorld import AutoWorldRegister +from .AutoWorld import AutoWorldRegister # noqa: E402 # Build the data package for each game. for world_name, world in AutoWorldRegister.world_types.items(): diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py index 5d865f3321..ccf747f15a 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -5,6 +5,7 @@ checking or launching the client, otherwise it will probably cause circular impo import asyncio +import enum import subprocess import traceback from typing import Any, Dict, Optional @@ -21,6 +22,13 @@ from .client import BizHawkClient, AutoBizHawkClientRegister EXPECTED_SCRIPT_VERSION = 1 +class AuthStatus(enum.IntEnum): + NOT_AUTHENTICATED = 0 + NEED_INFO = 1 + PENDING = 2 + AUTHENTICATED = 3 + + class BizHawkClientCommandProcessor(ClientCommandProcessor): def _cmd_bh(self): """Shows the current status of the client's connection to BizHawk""" @@ -35,6 +43,8 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor): class BizHawkClientContext(CommonContext): command_processor = BizHawkClientCommandProcessor + auth_status: AuthStatus + password_requested: bool client_handler: Optional[BizHawkClient] slot_data: Optional[Dict[str, Any]] = None rom_hash: Optional[str] = None @@ -45,6 +55,8 @@ class BizHawkClientContext(CommonContext): def __init__(self, server_address: Optional[str], password: Optional[str]): super().__init__(server_address, password) + self.auth_status = AuthStatus.NOT_AUTHENTICATED + self.password_requested = False self.client_handler = None self.bizhawk_ctx = BizHawkContext() self.watcher_timeout = 0.5 @@ -61,10 +73,41 @@ class BizHawkClientContext(CommonContext): def on_package(self, cmd, args): if cmd == "Connected": self.slot_data = args.get("slot_data", None) + self.auth_status = AuthStatus.AUTHENTICATED if self.client_handler is not None: self.client_handler.on_package(self, cmd, args) + async def server_auth(self, password_requested: bool = False): + self.password_requested = password_requested + + if self.bizhawk_ctx.connection_status != ConnectionStatus.CONNECTED: + logger.info("Awaiting connection to BizHawk before authenticating") + return + + if self.client_handler is None: + return + + # Ask handler to set auth + if self.auth is None: + self.auth_status = AuthStatus.NEED_INFO + await self.client_handler.set_auth(self) + + # Handler didn't set auth, ask user for slot name + if self.auth is None: + await self.get_username() + + if password_requested and not self.password: + self.auth_status = AuthStatus.NEED_INFO + await super(BizHawkClientContext, self).server_auth(password_requested) + + await self.send_connect() + self.auth_status = AuthStatus.PENDING + + async def disconnect(self, allow_autoreconnect: bool = False): + self.auth_status = AuthStatus.NOT_AUTHENTICATED + await super().disconnect(allow_autoreconnect) + async def _game_watcher(ctx: BizHawkClientContext): showed_connecting_message = False @@ -109,12 +152,13 @@ async def _game_watcher(ctx: BizHawkClientContext): rom_hash = await get_hash(ctx.bizhawk_ctx) if ctx.rom_hash is not None and ctx.rom_hash != rom_hash: - if ctx.server is not None: + if ctx.server is not None and not ctx.server.socket.closed: logger.info(f"ROM changed. Disconnecting from server.") - await ctx.disconnect(True) ctx.auth = None ctx.username = None + ctx.client_handler = None + await ctx.disconnect(False) ctx.rom_hash = rom_hash if ctx.client_handler is None: @@ -136,15 +180,14 @@ async def _game_watcher(ctx: BizHawkClientContext): except NotConnectedError: continue - # Get slot name and send `Connect` - if ctx.server is not None and ctx.username is None: - await ctx.client_handler.set_auth(ctx) - - if ctx.auth is None: - await ctx.get_username() - - await ctx.send_connect() + # Server auth + if ctx.server is not None and not ctx.server.socket.closed: + if ctx.auth_status == AuthStatus.NOT_AUTHENTICATED: + Utils.async_start(ctx.server_auth(ctx.password_requested)) + else: + ctx.auth_status = AuthStatus.NOT_AUTHENTICATED + # Call the handler's game watcher await ctx.client_handler.game_watcher(ctx) diff --git a/worlds/adventure/Rom.py b/worlds/adventure/Rom.py index 62c4019718..9f1ca3fe5e 100644 --- a/worlds/adventure/Rom.py +++ b/worlds/adventure/Rom.py @@ -6,9 +6,8 @@ from typing import Optional, Any import Utils from .Locations import AdventureLocation, LocationData -from Utils import OptionsType +from settings import get_settings from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer -from itertools import chain import bsdiff4 @@ -313,9 +312,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options: OptionsType = Utils.get_options() if not file_name: - file_name = options["adventure_options"]["rom_file"] + file_name = get_settings()["adventure_options"]["rom_file"] if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 22ef2a39a8..edc68473b9 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -520,7 +520,8 @@ class ALTTPSNIClient(SNIClient): gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): currently_dead = gamemode[0] in DEATH_MODES - await ctx.handle_deathlink_state(currently_dead) + await ctx.handle_deathlink_state(currently_dead, + ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "") gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1) game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 630d61e019..a68acf7288 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,7 +264,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if loc in all_state_base.events: all_state_base.events.remove(loc) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True) + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, + name="LttP Dungeon Items") dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 806a420f41..88a2d899fc 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -293,7 +293,6 @@ def generate_itempool(world): loc.access_rule = lambda state: has_triforce_pieces(state, player) region.locations.append(loc) - multiworld.clear_location_cache() multiworld.push_item(loc, ItemFactory('Triforce', player), False) loc.event = True diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 47cea8c20e..e1ae0cc6e6 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -786,8 +786,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # patch items - for location in world.get_locations(): - if location.player != player or location.address is None or location.shop_slot is not None: + for location in world.get_locations(player): + if location.address is None or location.shop_slot is not None: continue itemid = location.item.code if location.item is not None else 0x5A @@ -2247,7 +2247,7 @@ def write_strings(rom, world, player): tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!' hint_locations = HintLocations.copy() local_random.shuffle(hint_locations) - all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == player] + all_entrances = list(world.get_entrances(player)) local_random.shuffle(all_entrances) # First we take care of the one inconvenient dungeon in the appropriately simple shuffles. diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 1fddecd8f4..469f4f82ee 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -197,8 +197,13 @@ def global_rules(world, player): # determines which S&Q locations are available - hide from paths since it isn't an in-game location for exit in world.get_region('Menu', player).exits: exit.hide_path = True - - set_rule(world.get_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) + try: + old_man_sq = world.get_entrance('Old Man S&Q', player) + except KeyError: + pass # it doesn't exist, should be dungeon-only unittests + else: + old_man = world.get_location("Old Man", player) + set_rule(old_man_sq, lambda state: old_man.can_reach(state)) set_rule(world.get_location('Sunken Treasure', player), lambda state: state.has('Open Floodgate', player)) set_rule(world.get_location('Dark Blacksmith Ruins', player), lambda state: state.has('Return Smith', player)) @@ -1526,16 +1531,16 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): # Helper functions to determine if the moon pearl is required if inverted: def is_bunny(region): - return region.is_light_world + return region and region.is_light_world def is_link(region): - return region.is_dark_world + return region and region.is_dark_world else: def is_bunny(region): - return region.is_dark_world + return region and region.is_dark_world def is_link(region): - return region.is_light_world + return region and region.is_light_world def get_rule_to_add(region, location = None, connecting_entrance = None): # In OWG, a location can potentially be superbunny-mirror accessible or @@ -1603,21 +1608,20 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool): return options_to_access_rule(possible_options) # Add requirements for bunny-impassible caves if link is a bunny in them - for region in [world.get_region(name, player) for name in bunny_impassable_caves]: - + for region in (world.get_region(name, player) for name in bunny_impassable_caves): if not is_bunny(region): continue rule = get_rule_to_add(region) - for exit in region.exits: - add_rule(exit, rule) + for region_exit in region.exits: + add_rule(region_exit, rule) paradox_shop = world.get_region('Light World Death Mountain Shop', player) if is_bunny(paradox_shop): add_rule(paradox_shop.entrances[0], get_rule_to_add(paradox_shop)) # Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival - for entrance in world.get_entrances(): - if entrance.player == player and is_bunny(entrance.connected_region): + for entrance in world.get_entrances(player): + if is_bunny(entrance.connected_region): if world.logic[player] in ['minorglitches', 'owglitches', 'hybridglitches', 'nologic'] : if entrance.connected_region.type == LTTPRegionType.Dungeon: if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons(): diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index f17eb1eadb..c0f2e2236e 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -348,7 +348,6 @@ def create_shops(world, player: int): loc.item = ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player) loc.shop_slot_disabled = True shop.region.locations.append(loc) - world.clear_location_cache() class ShopData(NamedTuple): @@ -619,6 +618,4 @@ def create_dynamic_shop_locations(world, player): if shop.type == ShopType.TakeAny: loc.shop_slot_disabled = True shop.region.locations.append(loc) - world.clear_location_cache() - loc.shop_slot = i diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index 4b6bc54111..a6aefc7412 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -31,7 +31,7 @@ def fake_pearl_state(state, player): if state.has('Moon Pearl', player): return state fake_state = state.copy() - fake_state.prog_items['Moon Pearl', player] += 1 + fake_state.prog_items[player]['Moon Pearl'] += 1 return fake_state diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 65e36da3bd..d89e65c59d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -470,7 +470,8 @@ class ALTTPWorld(World): prizepool = unplaced_prizes.copy() prize_locs = empty_crystal_locations.copy() world.random.shuffle(prize_locs) - fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True) + fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True, + name="LttP Dungeon Prizes") except FillError as e: lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt) @@ -585,27 +586,26 @@ class ALTTPWorld(World): for player in checks_in_area: checks_in_area[player]["Total"] = 0 - - for location in multiworld.get_locations(): - if location.game == cls.game and type(location.address) is int: - main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance) - if location.parent_region.dungeon: - dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', - 'Inverted Ganons Tower': 'Ganons Tower'} \ - .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) - checks_in_area[location.player][dungeonname].append(location.address) - elif location.parent_region.type == LTTPRegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif location.parent_region.type == LTTPRegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) - elif main_entrance.parent_region.type == LTTPRegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) - else: - assert False, "Unknown Location area." - # TODO: remove Total as it's duplicated data and breaks consistent typing - checks_in_area[location.player]["Total"] += 1 + for location in multiworld.get_locations(player): + if location.game == cls.game and type(location.address) is int: + main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance) + if location.parent_region.dungeon: + dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', + 'Inverted Ganons Tower': 'Ganons Tower'} \ + .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) + checks_in_area[location.player][dungeonname].append(location.address) + elif location.parent_region.type == LTTPRegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif location.parent_region.type == LTTPRegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) + elif main_entrance.parent_region.type == LTTPRegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) + else: + assert False, "Unknown Location area." + # TODO: remove Total as it's duplicated data and breaks consistent typing + checks_in_area[location.player]["Total"] += 1 multidata["checks_in_area"].update(checks_in_area) @@ -830,4 +830,4 @@ class ALttPLogic(LogicMixin): return True if self.multiworld.smallkey_shuffle[player] == smallkey_shuffle.option_universal: return can_buy_unlimited(self, 'Small Key (Universal)', player) - return self.prog_items[item, player] >= count + return self.prog_items[player][item] >= count diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 94c30c3493..8ca2791dcf 100644 --- a/worlds/alttp/test/dungeons/TestDungeon.py +++ b/worlds/alttp/test/dungeons/TestDungeon.py @@ -1,5 +1,5 @@ from BaseClasses import CollectionState, ItemClassification -from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool +from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.EntranceShuffle import mandatory_connections, connect_simple from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory diff --git a/worlds/archipidle/Rules.py b/worlds/archipidle/Rules.py index cdd48e7604..3bf4bad475 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -5,12 +5,7 @@ from ..generic.Rules import set_rule class ArchipIDLELogic(LogicMixin): def _archipidle_location_is_accessible(self, player_id, items_required): - items_received = 0 - for item in self.prog_items: - if item[1] == player_id: - items_received += 1 - - return items_received >= items_required + return sum(self.prog_items[player_id].values()) >= items_required def set_rules(world: MultiWorld, player: int): diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index ea304d22ed..127a1dc776 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -67,6 +67,7 @@ class StartingLocation(ChoiceIsRandom): class Ending(Choice): """Choose which ending is required to complete the game. + Talking to Tirso in Albero will tell you the selected ending for the current game. Ending A: Collect all thorn upgrades. Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.""" display_name = "Ending" diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 4218fa94cf..5d88292131 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -578,11 +578,12 @@ def rules(blasphemousworld): or state.has("Purified Hand of the Nun", player) or state.has("D01Z02S03[NW]", player) and ( - can_cross_gap(state, logic, player, 1) + can_cross_gap(state, logic, player, 2) or state.has("Lorquiana", player) or aubade(state, player) or state.has("Cantina of the Blue Rose", player) or charge_beam(state, player) + or state.has("Ranged Skill", player) ) )) set_rule(world.get_location("Albero: Lvdovico's 1st reward", player), @@ -702,10 +703,11 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player), lambda state: ( - can_cross_gap(state, logic, player, 1) + can_cross_gap(state, logic, player, 2) or aubade(state, player) or charge_beam(state, player) - or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) + or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", \ + "Cloistered Ruby", "Ranged Skill"}, player) or precise_skips_allowed(logic) )) # Doors diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md index 15223213ac..1ff7f5a903 100644 --- a/worlds/blasphemous/docs/en_Blasphemous.md +++ b/worlds/blasphemous/docs/en_Blasphemous.md @@ -19,6 +19,7 @@ In addition, there are other changes to the game that make it better optimized f - The Apodictic Heart of Mea Culpa can be unequipped. - Dying with the Immaculate Bead is unnecessary, it is automatically upgraded to the Weight of True Guilt. - If the option is enabled, the 34 corpses in game will have their messages changed to give hints about certain items and locations. The Shroud of Dreamt Sins is not required to hear them. +- Talking to Tirso in Albero will tell you the selected ending for the current game. ## What has been changed about the side quests? diff --git a/worlds/checksfinder/__init__.py b/worlds/checksfinder/__init__.py index feff148651..4978500da0 100644 --- a/worlds/checksfinder/__init__.py +++ b/worlds/checksfinder/__init__.py @@ -69,8 +69,8 @@ class ChecksFinderWorld(World): def create_regions(self): menu = Region("Menu", self.player, self.multiworld) board = Region("Board", self.player, self.multiworld) - board.locations = [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board) - for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name] + board.locations += [ChecksFinderAdvancement(self.player, loc_name, loc_data.id, board) + for loc_name, loc_data in advancement_table.items() if loc_data.region == board.name] connection = Entrance(self.player, "New Board", menu) menu.exits.append(connection) diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md index bd82660b09..96fb0529df 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -14,11 +14,18 @@ many checks as you have gained items, plus five to start with being available. ## When the player receives an item, what happens? When the player receives an item in ChecksFinder, it either can make the future boards they play be bigger in width or -height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being -bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a number +height, or add a new bomb to the future boards, with a limit to having up to one fifth of the _current_ board being +bombs. The items you have gained _before_ the current board was made will be said at the bottom of the screen as a +number next to an icon, the number is how many you have gotten and the icon represents which item it is. ## What is the victory condition? Victory is achieved when the player wins a board they were given after they have received all of their Map Width, Map -Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received. \ No newline at end of file +Height, and Map Bomb items. The game will say at the bottom of the screen how many of each you have received. + +## Unique Local Commands + +The following command is only available when using the ChecksFinderClient to play with Archipelago. + +- `/resync` Manually trigger a resync. diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index a11e5c504e..5792d9c3ab 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -12,11 +12,11 @@ def create_event(player, event: str) -> DLCQuestItem: def has_enough_coin(player: int, coin: int): - return lambda state: state.prog_items[" coins", player] >= coin + return lambda state: state.prog_items[player][" coins"] >= coin def has_enough_coin_freemium(player: int, coin: int): - return lambda state: state.prog_items[" coins freemium", player] >= coin + return lambda state: state.prog_items[player][" coins freemium"] >= coin def set_rules(world, player, World_Options: Options.DLCQuestOptions): diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 54d27f7b65..e4e0a29274 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -92,7 +92,7 @@ class DLCqworld(World): if change: suffix = item.coin_suffix if suffix: - state.prog_items[suffix, self.player] += item.coins + state.prog_items[self.player][suffix] += item.coins return change def remove(self, state: CollectionState, item: DLCQuestItem) -> bool: @@ -100,5 +100,5 @@ class DLCqworld(World): if change: suffix = item.coin_suffix if suffix: - state.prog_items[suffix, self.player] -= item.coins + state.prog_items[self.player][suffix] -= item.coins return change diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 8962919743..59fa85d916 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -26,6 +26,7 @@ All local and remote items appear the same. Final Fantasy will say that you rece emulator will display what was found external to the in-game text box. ## Unique Local Commands -The following command is only available when using the FF1Client for the Final Fantasy Randomizer. +The following commands are only available when using the FF1Client for the Final Fantasy Randomizer. - `/nes` Shows the current status of the NES connection. +- `/toggle_msgs` Toggle displaying messages in EmuHawk diff --git a/worlds/hk/Items.py b/worlds/hk/Items.py index a9acbf48f3..def5c32981 100644 --- a/worlds/hk/Items.py +++ b/worlds/hk/Items.py @@ -19,18 +19,43 @@ lookup_type_to_names: Dict[str, Set[str]] = {} for item, item_data in item_table.items(): lookup_type_to_names.setdefault(item_data.type, set()).add(item) -item_name_groups = {group: lookup_type_to_names[group] for group in ("Skill", "Charm", "Mask", "Vessel", - "Relic", "Root", "Map", "Stag", "Cocoon", - "Soul", "DreamWarrior", "DreamBoss")} - directionals = ('', 'Left_', 'Right_') - -item_name_groups.update({ +item_name_groups = ({ + "BossEssence": lookup_type_to_names["DreamWarrior"] | lookup_type_to_names["DreamBoss"], + "BossGeo": lookup_type_to_names["Boss_Geo"], + "CDash": {x + "Crystal_Heart" for x in directionals}, + "Charms": lookup_type_to_names["Charm"], + "CharmNotches": lookup_type_to_names["Notch"], + "Claw": {x + "Mantis_Claw" for x in directionals}, + "Cloak": {x + "Mothwing_Cloak" for x in directionals} | {"Shade_Cloak", "Split_Shade_Cloak"}, + "Dive": {"Desolate_Dive", "Descending_Dark"}, + "LifebloodCocoons": lookup_type_to_names["Cocoon"], "Dreamers": {"Herrah", "Monomon", "Lurien"}, - "Cloak": {x + 'Mothwing_Cloak' for x in directionals} | {'Shade_Cloak', 'Split_Shade_Cloak'}, - "Claw": {x + 'Mantis_Claw' for x in directionals}, - "CDash": {x + 'Crystal_Heart' for x in directionals}, - "Fragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"}, + "Fireball": {"Vengeful_Spirit", "Shade_Soul"}, + "GeoChests": lookup_type_to_names["Geo"], + "GeoRocks": lookup_type_to_names["Rock"], + "GrimmkinFlames": lookup_type_to_names["Flame"], + "Grubs": lookup_type_to_names["Grub"], + "JournalEntries": lookup_type_to_names["Journal"], + "JunkPitChests": lookup_type_to_names["JunkPitChest"], + "Keys": lookup_type_to_names["Key"], + "LoreTablets": lookup_type_to_names["Lore"] | lookup_type_to_names["PalaceLore"], + "Maps": lookup_type_to_names["Map"], + "MaskShards": lookup_type_to_names["Mask"], + "Mimics": lookup_type_to_names["Mimic"], + "Nail": lookup_type_to_names["CursedNail"], + "PalaceJournal": {"Journal_Entry-Seal_of_Binding"}, + "PalaceLore": lookup_type_to_names["PalaceLore"], + "PalaceTotem": {"Soul_Totem-Palace", "Soul_Totem-Path_of_Pain"}, + "RancidEggs": lookup_type_to_names["Egg"], + "Relics": lookup_type_to_names["Relic"], + "Scream": {"Howling_Wraiths", "Abyss_Shriek"}, + "Skills": lookup_type_to_names["Skill"], + "SoulTotems": lookup_type_to_names["Soul"], + "Stags": lookup_type_to_names["Stag"], + "VesselFragments": lookup_type_to_names["Vessel"], + "WhisperingRoots": lookup_type_to_names["Root"], + "WhiteFragments": {"Queen_Fragment", "King_Fragment", "Void_Heart"}, }) item_name_groups['Horizontal'] = item_name_groups['Cloak'] | item_name_groups['CDash'] item_name_groups['Vertical'] = item_name_groups['Claw'] | {'Monarch_Wings'} diff --git a/worlds/hk/Rules.py b/worlds/hk/Rules.py index 4fe4160b4c..2dc512eca7 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -1,5 +1,4 @@ from ..generic.Rules import set_rule, add_rule -from BaseClasses import MultiWorld from ..AutoWorld import World from .GeneratedRules import set_generated_rules from typing import NamedTuple @@ -39,14 +38,12 @@ def hk_set_rule(hk_world: World, location: str, rule): def set_rules(hk_world: World): player = hk_world.player - world = hk_world.multiworld set_generated_rules(hk_world, hk_set_rule) # Shop costs - for region in world.get_regions(player): - for location in region.locations: - if location.costs: - for term, amount in location.costs.items(): - if term == "GEO": # No geo logic! - continue - add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) + for location in hk_world.multiworld.get_locations(player): + if location.costs: + for term, amount in location.costs.items(): + if term == "GEO": # No geo logic! + continue + add_rule(location, lambda state, term=term, amount=amount: state.count(term, player) >= amount) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 1a9d4b5d61..c16a108cd1 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -517,12 +517,12 @@ class HKWorld(World): change = super(HKWorld, self).collect(state, item) if change: for effect_name, effect_value in item_effects.get(item.name, {}).items(): - state.prog_items[effect_name, item.player] += effect_value + state.prog_items[item.player][effect_name] += effect_value if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}: - if state.prog_items.get(('RIGHTDASH', item.player), 0) and \ - state.prog_items.get(('LEFTDASH', item.player), 0): - (state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player]) = \ - ([max(state.prog_items["RIGHTDASH", item.player], state.prog_items["LEFTDASH", item.player])] * 2) + if state.prog_items[item.player].get('RIGHTDASH', 0) and \ + state.prog_items[item.player].get('LEFTDASH', 0): + (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \ + ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2) return change def remove(self, state, item: HKItem) -> bool: @@ -530,9 +530,9 @@ class HKWorld(World): if change: for effect_name, effect_value in item_effects.get(item.name, {}).items(): - if state.prog_items[effect_name, item.player] == effect_value: - del state.prog_items[effect_name, item.player] - state.prog_items[effect_name, item.player] -= effect_value + if state.prog_items[item.player][effect_name] == effect_value: + del state.prog_items[item.player][effect_name] + state.prog_items[item.player][effect_name] -= effect_value return change diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 6c89db3891..c7b127ef2b 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -124,13 +124,13 @@ class GameStateAdapater: # Don't allow any money usage if you can't get back wasted rupees if item == "RUPEES": if can_farm_rupees(self.state, self.player): - return self.state.prog_items["RUPEES", self.player] + return self.state.prog_items[self.player]["RUPEES"] return 0 elif item.endswith("_USED"): return 0 else: item = ladxr_item_to_la_item_name[item] - return self.state.prog_items.get((item, self.player), default) + return self.state.prog_items[self.player].get(item, default) class LinksAwakeningEntrance(Entrance): @@ -219,7 +219,7 @@ def create_regions_from_ladxr(player, multiworld, logic): r = LinksAwakeningRegion( name=name, ladxr_region=l, hint="", player=player, world=multiworld) - r.locations = [LinksAwakeningLocation(player, r, i) for i in l.items] + r.locations += [LinksAwakeningLocation(player, r, i) for i in l.items] regions[l] = r for ladxr_location in logic.location_list: diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 1d6c85dd64..eaaea5be2f 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -231,9 +231,7 @@ class LinksAwakeningWorld(World): # Find instrument, lock # TODO: we should be able to pinpoint the region we want, save a lookup table please found = False - for r in self.multiworld.get_regions(): - if r.player != self.player: - continue + for r in self.multiworld.get_regions(self.player): if r.dungeon_index != item.item_data.dungeon_index: continue for loc in r.locations: @@ -269,10 +267,7 @@ class LinksAwakeningWorld(World): event_location.place_locked_item(self.create_event("Can Play Trendy Game")) self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] - for r in self.multiworld.get_regions(): - if r.player != self.player: - continue - + for r in self.multiworld.get_regions(self.player): # Set aside dungeon locations if r.dungeon_index: self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations @@ -518,7 +513,7 @@ class LinksAwakeningWorld(World): change = super().collect(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items["RUPEES", item.player] += rupees + state.prog_items[item.player]["RUPEES"] += rupees return change @@ -526,6 +521,6 @@ class LinksAwakeningWorld(World): change = super().remove(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items["RUPEES", item.player] -= rupees + state.prog_items[item.player]["RUPEES"] -= rupees return change diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py index 1da8d235a6..446668d392 100644 --- a/worlds/lufia2ac/Rom.py +++ b/worlds/lufia2ac/Rom.py @@ -3,7 +3,7 @@ import os from typing import Optional import Utils -from Utils import OptionsType +from settings import get_settings from worlds.Files import APDeltaPatch L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d" @@ -35,9 +35,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: def get_base_rom_path(file_name: str = "") -> str: - options: OptionsType = Utils.get_options() if not file_name: - file_name = options["lufia2ac_options"]["rom_file"] + file_name = get_settings()["lufia2ac_options"]["rom_file"] if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name diff --git a/worlds/meritous/Regions.py b/worlds/meritous/Regions.py index 2c66a024ca..de34570d02 100644 --- a/worlds/meritous/Regions.py +++ b/worlds/meritous/Regions.py @@ -54,12 +54,12 @@ def create_regions(world: MultiWorld, player: int): world.regions.append(boss_region) region_final_boss = Region("Final Boss", player, world) - region_final_boss.locations = [MeritousLocation( + region_final_boss.locations += [MeritousLocation( player, "Wervyn Anixil", None, region_final_boss)] world.regions.append(region_final_boss) region_tfb = Region("True Final Boss", player, world) - region_tfb.locations = [MeritousLocation( + region_tfb.locations += [MeritousLocation( player, "Wervyn Anixil?", None, region_tfb)] world.regions.append(region_tfb) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 0771989ffc..3fe13a3cb4 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -188,6 +188,6 @@ class MessengerWorld(World): shard_count = int(item.name.strip("Time Shard ()")) if remove: shard_count = -shard_count - state.prog_items["Shards", self.player] += shard_count + state.prog_items[self.player]["Shards"] += shard_count return super().collect_item(state, item, remove) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index fa992e1e11..187f1fdf19 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -173,7 +173,7 @@ class MinecraftWorld(World): def generate_output(self, output_directory: str) -> None: data = self._get_mc_data() - filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc" + filename = f"{self.multiworld.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) diff --git a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md index 854034d5a8..7ffa4665fd 100644 --- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md +++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md @@ -72,3 +72,10 @@ what item and what player is receiving the item Whenever you have an item pending, the next time you are not in a battle, menu, or dialog box, you will receive a message on screen notifying you of the item and sender, and the item will be added directly to your inventory. + +## Unique Local Commands + +The following commands are only available when using the MMBN3Client to play with Archipelago. + +- `/gba` Check GBA Connection State +- `/debug` Toggle the Debug Text overlay in ROM diff --git a/worlds/musedash/MuseDashData.txt b/worlds/musedash/MuseDashData.txt index bd07fef7af..5b3ef40e54 100644 --- a/worlds/musedash/MuseDashData.txt +++ b/worlds/musedash/MuseDashData.txt @@ -404,7 +404,7 @@ trippers feeling!|8-4|Give Up TREATMENT Vol.3|True|5|7|9|11 Lilith ambivalence lovers|8-5|Give Up TREATMENT Vol.3|False|5|8|10| Brave My Soul|7-0|Give Up TREATMENT Vol.2|False|4|6|8| Halcyon|7-1|Give Up TREATMENT Vol.2|False|4|7|10| -Crimson Nightingle|7-2|Give Up TREATMENT Vol.2|True|4|7|10| +Crimson Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10| Invader|7-3|Give Up TREATMENT Vol.2|True|3|7|11| Lyrith|7-4|Give Up TREATMENT Vol.2|False|5|7|10| GOODBOUNCE|7-5|Give Up TREATMENT Vol.2|False|4|6|9| @@ -488,4 +488,11 @@ Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10| The Vampire|66-6|Miku in Museland|False|4|6|9| Future Eve|66-7|Miku in Museland|False|4|8|11| Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10| -Shun-ran|66-9|Miku in Museland|False|4|7|9| \ No newline at end of file +Shun-ran|66-9|Miku in Museland|False|4|7|9| +NICE TYPE feat. monii|43-41|MD Plus Project|True|3|6|8| +Rainy Angel|67-0|Happy Otaku Pack Vol.18|True|4|6|9|11 +Gullinkambi|67-1|Happy Otaku Pack Vol.18|True|4|7|10| +RakiRaki Rebuilders!!!|67-2|Happy Otaku Pack Vol.18|True|5|7|10| +Laniakea|67-3|Happy Otaku Pack Vol.18|False|5|8|10| +OTTAMA GAZER|67-4|Happy Otaku Pack Vol.18|True|5|8|10| +Sleep Tight feat.Macoto|67-5|Happy Otaku Pack Vol.18|True|3|5|8| \ No newline at end of file diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index 63ce123c93..bfe321b64a 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -49,7 +49,7 @@ class MuseDashWorld(World): game = "Muse Dash" options_dataclass: ClassVar[Type[PerGameCommonOptions]] = MuseDashOptions topology_present = False - data_version = 10 + data_version = 11 web = MuseDashWebWorld() # Necessary Data diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index ca53c96233..c859a80394 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/Items.py @@ -44,20 +44,18 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]: return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else [] -def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]: - filler_pool = filler_weights.copy() +def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]: + filler_pool = weights.copy() if multiworld.bad_effects[player].value == 0: del filler_pool["Trap"] - return multiworld.random.choices( - population=list(filler_pool.keys()), - weights=list(filler_pool.values()), - k=random_count - ) + return multiworld.random.choices(population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=count) def create_all_items(multiworld: MultiWorld, player: int) -> None: - sum_locations = len(multiworld.get_unfilled_locations(player)) + locations_to_fill = len(multiworld.get_unfilled_locations(player)) itempool = ( create_fixed_item_pool() @@ -66,9 +64,18 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None: + create_kantele(multiworld.victory_condition[player]) ) - random_count = sum_locations - len(itempool) - itempool += create_random_items(multiworld, player, random_count) + # if there's not enough shop-allowed items in the pool, we can encounter gen issues + # 39 is the number of shop-valid items we need to guarantee + if len(itempool) < 39: + itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool)) + # this is so that it passes tests and gens if you have minimal locations and only one player + if multiworld.players == 1: + for location in multiworld.get_unfilled_locations(player): + if "Shop Item" in location.name: + location.item = create_item(player, itempool.pop()) + locations_to_fill = len(multiworld.get_unfilled_locations(player)) + itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool)) multiworld.itempool += [create_item(player, name) for name in itempool] @@ -84,8 +91,8 @@ item_table: Dict[str, ItemData] = { "Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful), "Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful), "Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful), - "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful), - "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful), + "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful, 1), + "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), @@ -95,43 +102,46 @@ item_table: Dict[str, ItemData] = { "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), - "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful), + "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful, 1), "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), "Random Potion": ItemData(110023, "Items", ItemClassification.filler), "Secret Potion": ItemData(110024, "Items", ItemClassification.filler), "Powder Pouch": ItemData(110025, "Items", ItemClassification.filler), "Chaos Die": ItemData(110026, "Items", ItemClassification.filler), "Greed Die": ItemData(110027, "Items", ItemClassification.filler), - "Kammi": ItemData(110028, "Items", ItemClassification.filler), - "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler), + "Kammi": ItemData(110028, "Items", ItemClassification.filler, 1), + "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1), "Sädekivi": ItemData(110030, "Items", ItemClassification.filler), "Broken Wand": ItemData(110031, "Items", ItemClassification.filler), +} +shop_only_filler_weights: Dict[str, int] = { + "Trap": 15, + "Extra Max HP": 25, + "Spell Refresher": 20, + "Wand (Tier 1)": 10, + "Wand (Tier 2)": 8, + "Wand (Tier 3)": 7, + "Wand (Tier 4)": 6, + "Wand (Tier 5)": 5, + "Wand (Tier 6)": 4, + "Extra Life Perk": 10, } filler_weights: Dict[str, int] = { - "Trap": 15, - "Extra Max HP": 25, - "Spell Refresher": 20, - "Potion": 40, - "Gold (200)": 15, - "Gold (1000)": 6, - "Wand (Tier 1)": 10, - "Wand (Tier 2)": 8, - "Wand (Tier 3)": 7, - "Wand (Tier 4)": 6, - "Wand (Tier 5)": 5, - "Wand (Tier 6)": 4, - "Extra Life Perk": 10, - "Random Potion": 9, - "Secret Potion": 10, - "Powder Pouch": 10, - "Chaos Die": 4, - "Greed Die": 4, - "Kammi": 4, - "Refreshing Gourd": 4, - "Sädekivi": 3, - "Broken Wand": 10, + **shop_only_filler_weights, + "Gold (200)": 15, + "Gold (1000)": 6, + "Potion": 40, + "Random Potion": 9, + "Secret Potion": 10, + "Powder Pouch": 10, + "Chaos Die": 4, + "Greed Die": 4, + "Kammi": 4, + "Refreshing Gourd": 4, + "Sädekivi": 3, + "Broken Wand": 10, } diff --git a/worlds/noita/Regions.py b/worlds/noita/Regions.py index a239b437d7..561d483b48 100644 --- a/worlds/noita/Regions.py +++ b/worlds/noita/Regions.py @@ -1,5 +1,5 @@ # Regions are areas in your game that you travel to. -from typing import Dict, Set +from typing import Dict, Set, List from BaseClasses import Entrance, MultiWorld, Region from . import Locations @@ -79,70 +79,46 @@ def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> N # - Lake is connected to The Laboratory, since the boss is hard without specific set-ups (which means late game) # - Snowy Depths connects to Lava Lake orb since you need digging for it, so fairly early is acceptable # - Ancient Laboratory is connected to the Coal Pits, so that Ylialkemisti isn't sphere 1 -noita_connections: Dict[str, Set[str]] = { - "Menu": {"Forest"}, - "Forest": {"Mines", "Floating Island", "Desert", "Snowy Wasteland"}, - "Snowy Wasteland": {"Forest"}, - "Frozen Vault": {"The Vault"}, - "Lake": {"The Laboratory"}, - "Desert": {"Forest"}, - "Floating Island": {"Forest"}, - "Pyramid": {"Hiisi Base"}, - "Overgrown Cavern": {"Sandcave", "Undeground Jungle"}, - "Sandcave": {"Overgrown Cavern"}, +noita_connections: Dict[str, List[str]] = { + "Menu": ["Forest"], + "Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"], + "Frozen Vault": ["The Vault"], + "Overgrown Cavern": ["Sandcave"], ### - "Mines": {"Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake", "Forest"}, - "Collapsed Mines": {"Mines", "Dark Cave"}, - "Lava Lake": {"Mines", "Abyss Orb Room"}, - "Abyss Orb Room": {"Lava Lake"}, - "Below Lava Lake": {"Snowy Depths"}, - "Dark Cave": {"Collapsed Mines"}, - "Ancient Laboratory": {"Coal Pits"}, + "Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"], + "Lava Lake": ["Abyss Orb Room"], ### - "Coal Pits Holy Mountain": {"Coal Pits"}, - "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"}, - "Fungal Caverns": {"Coal Pits"}, + "Coal Pits Holy Mountain": ["Coal Pits"], + "Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"], ### - "Snowy Depths Holy Mountain": {"Snowy Depths"}, - "Snowy Depths": {"Snowy Depths Holy Mountain", "Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"}, - "Magical Temple": {"Snowy Depths"}, + "Snowy Depths Holy Mountain": ["Snowy Depths"], + "Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"], ### - "Hiisi Base Holy Mountain": {"Hiisi Base"}, - "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"}, - "Secret Shop": {"Hiisi Base"}, + "Hiisi Base Holy Mountain": ["Hiisi Base"], + "Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"], ### - "Underground Jungle Holy Mountain": {"Underground Jungle"}, - "Underground Jungle": {"Underground Jungle Holy Mountain", "Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", - "Lukki Lair"}, - "Dragoncave": {"Underground Jungle"}, - "Lukki Lair": {"Underground Jungle", "Snow Chasm", "Frozen Vault"}, - "Snow Chasm": {}, + "Underground Jungle Holy Mountain": ["Underground Jungle"], + "Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm"], ### - "Vault Holy Mountain": {"The Vault"}, - "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"}, + "Vault Holy Mountain": ["The Vault"], + "The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"], ### - "Temple of the Art Holy Mountain": {"Temple of the Art"}, - "Temple of the Art": {"Temple of the Art Holy Mountain", "Laboratory Holy Mountain", "The Tower", - "Wizards' Den"}, - "Wizards' Den": {"Temple of the Art", "Powerplant"}, - "Powerplant": {"Wizards' Den", "Deep Underground"}, - "The Tower": {"Forest"}, - "Deep Underground": {}, + "Temple of the Art Holy Mountain": ["Temple of the Art"], + "Temple of the Art": ["Laboratory Holy Mountain", "The Tower", "Wizards' Den"], + "Wizards' Den": ["Powerplant"], + "Powerplant": ["Deep Underground"], ### - "Laboratory Holy Mountain": {"The Laboratory"}, - "The Laboratory": {"Laboratory Holy Mountain", "The Work", "Friend Cave", "The Work (Hell)", "Lake"}, - "Friend Cave": {}, - "The Work": {}, - "The Work (Hell)": {}, + "Laboratory Holy Mountain": ["The Laboratory"], + "The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake"], ### } -noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values()) +noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values())) diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py index 3eb6be5a7c..808dd3a200 100644 --- a/worlds/noita/Rules.py +++ b/worlds/noita/Rules.py @@ -44,12 +44,10 @@ wand_tiers: List[str] = [ "Wand (Tier 6)", # Temple of the Art ] - items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", "Powder Pouch"] - perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys())) @@ -155,11 +153,12 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: def create_all_rules(multiworld: MultiWorld, player: int) -> None: - ban_items_from_shops(multiworld, player) - ban_early_high_tier_wands(multiworld, player) - lock_holy_mountains_into_spheres(multiworld, player) - holy_mountain_unlock_conditions(multiworld, player) - biome_unlock_conditions(multiworld, player) + if multiworld.players > 1: + ban_items_from_shops(multiworld, player) + ban_early_high_tier_wands(multiworld, player) + lock_holy_mountains_into_spheres(multiworld, player) + holy_mountain_unlock_conditions(multiworld, player) + biome_unlock_conditions(multiworld, player) victory_unlock_conditions(multiworld, player) # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py index e480c957a6..6c4b6428f5 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -1,6 +1,4 @@ - from BaseClasses import Entrance -from .Regions import TimeOfDay class OOTEntrance(Entrance): game: str = 'Ocarina of Time' @@ -29,16 +27,16 @@ class OOTEntrance(Entrance): self.connected_region = None return previously_connected - def get_new_target(self): + def get_new_target(self, pool_type): root = self.multiworld.get_region('Root Exits', self.player) - target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root) + target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root) target_entrance.connect(self.connected_region) target_entrance.replaces = self root.exits.append(target_entrance) return target_entrance - def assume_reachable(self): + def assume_reachable(self, pool_type): if self.assumed == None: - self.assumed = self.get_new_target() + self.assumed = self.get_new_target(pool_type) self.disconnect() return self.assumed diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index 3c1b2d78c6..bbdc30490c 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -2,6 +2,7 @@ from itertools import chain import logging from worlds.generic.Rules import set_rule, add_rule +from BaseClasses import CollectionState from .Hints import get_hint_area, HintAreaNotFound from .Regions import TimeOfDay @@ -25,12 +26,12 @@ def set_all_entrances_data(world, player): return_entrance.data['index'] = 0x7FFF -def assume_entrance_pool(entrance_pool, ootworld): +def assume_entrance_pool(entrance_pool, ootworld, pool_type): assumed_pool = [] for entrance in entrance_pool: - assumed_forward = entrance.assume_reachable() + assumed_forward = entrance.assume_reachable(pool_type) if entrance.reverse != None and not ootworld.decouple_entrances: - assumed_return = entrance.reverse.assume_reachable() + assumed_return = entrance.reverse.assume_reachable(pool_type) if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)): if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \ (entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances): @@ -41,15 +42,15 @@ def assume_entrance_pool(entrance_pool, ootworld): return assumed_pool -def build_one_way_targets(world, types_to_include, exclude=(), target_region_names=()): +def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()): one_way_entrances = [] for pool_type in types_to_include: one_way_entrances += world.get_shufflable_entrances(type=pool_type) valid_one_way_entrances = list(filter(lambda entrance: entrance.name not in exclude, one_way_entrances)) if target_region_names: - return [entrance.get_new_target() for entrance in valid_one_way_entrances + return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances if entrance.connected_region.name in target_region_names] - return [entrance.get_new_target() for entrance in valid_one_way_entrances] + return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances] # Abbreviations @@ -423,14 +424,14 @@ multi_interior_regions = { } interior_entrance_bias = { - 'Kakariko Village -> Kak Potion Shop Front': 4, - 'Kak Backyard -> Kak Potion Shop Back': 4, - 'Kakariko Village -> Kak Impas House': 3, - 'Kak Impas Ledge -> Kak Impas House Back': 3, - 'Goron City -> GC Shop': 2, - 'Zoras Domain -> ZD Shop': 2, + 'ToT Entrance -> Temple of Time': 4, + 'Kakariko Village -> Kak Potion Shop Front': 3, + 'Kak Backyard -> Kak Potion Shop Back': 3, + 'Kakariko Village -> Kak Impas House': 2, + 'Kak Impas Ledge -> Kak Impas House Back': 2, 'Market Entrance -> Market Guard House': 2, - 'ToT Entrance -> Temple of Time': 1, + 'Goron City -> GC Shop': 1, + 'Zoras Domain -> ZD Shop': 1, } @@ -443,7 +444,8 @@ def shuffle_random_entrances(ootworld): player = ootworld.player # Gather locations to keep reachable for validation - all_state = world.get_all_state(use_cache=True) + all_state = ootworld.get_state_with_complete_itempool() + all_state.sweep_for_events(locations=ootworld.get_locations()) locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))} # Set entrance data for all entrances @@ -523,12 +525,12 @@ def shuffle_random_entrances(ootworld): for pool_type, entrance_pool in one_way_entrance_pools.items(): if pool_type == 'OwlDrop': valid_target_types = ('WarpSong', 'OwlDrop', 'Overworld', 'Extra') - one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) for target in one_way_target_entrance_pools[pool_type]: set_rule(target, lambda state: state._oot_reach_as_age(target.parent_region, 'child', player)) elif pool_type in {'Spawn', 'WarpSong'}: valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra') - one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, pool_type, valid_target_types) # Ensure that the last entrance doesn't assume the rest of the targets are reachable for target in one_way_target_entrance_pools[pool_type]: add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))()) @@ -538,14 +540,11 @@ def shuffle_random_entrances(ootworld): target_entrance_pools = {} for pool_type, entrance_pool in entrance_pools.items(): - target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) + target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld, pool_type) # Build all_state and none_state all_state = ootworld.get_state_with_complete_itempool() - none_state = all_state.copy() - for item_tuple in none_state.prog_items: - if item_tuple[1] == player: - none_state.prog_items[item_tuple] = 0 + none_state = CollectionState(ootworld.multiworld) # Plando entrances if world.plando_connections[player]: @@ -628,7 +627,7 @@ def shuffle_random_entrances(ootworld): logging.getLogger('').error(f'Root Exit: {exit} -> {exit.connected_region}') logging.getLogger('').error(f'Root has too many entrances left after shuffling entrances') # Game is beatable - new_all_state = world.get_all_state(use_cache=False) + new_all_state = ootworld.get_state_with_complete_itempool() if not world.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world @@ -700,7 +699,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}') -def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20): +def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=10): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -745,7 +744,6 @@ def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollback def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entrances): - world = ootworld.multiworld player = ootworld.player # Disconnect all root assumed entrances and save original connections @@ -755,7 +753,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran if entrance.connected_region: original_connected_regions[entrance] = entrance.disconnect() - all_state = world.get_all_state(use_cache=False) + all_state = ootworld.get_state_with_complete_itempool() restrictive_entrances = [] soft_entrances = [] @@ -793,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all all_state = all_state_orig.copy() none_state = none_state_orig.copy() - all_state.sweep_for_events() - none_state.sweep_for_events() + all_state.sweep_for_events(locations=ootworld.get_locations()) + none_state.sweep_for_events(locations=ootworld.get_locations()) if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions: time_travel_state = none_state.copy() diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index f83b34183c..0f1d3f4dcb 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -2182,7 +2182,7 @@ def patch_rom(world, rom): 'Shadow Temple': ("the \x05\x45Shadow Temple", 'Bongo Bongo', 0x7f, 0xa3), } for dungeon in world.dungeon_mq: - if dungeon in ['Gerudo Training Ground', 'Ganons Castle']: + if dungeon in ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']: pass elif dungeon in ['Bottom of the Well', 'Ice Cavern']: dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon] diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index fa198e0ce1..529411f6fc 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -1,8 +1,12 @@ from collections import deque import logging +import typing from .Regions import TimeOfDay +from .DungeonList import dungeon_table +from .Hints import HintArea from .Items import oot_is_item_of_type +from .LocationList import dungeon_song_locations from BaseClasses import CollectionState from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item @@ -150,11 +154,16 @@ def set_rules(ootworld): location = world.get_location('Forest Temple MQ First Room Chest', player) forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player) - if ootworld.shuffle_song_items == 'song' and not ootworld.songs_as_items: + if ootworld.shuffle_song_items in {'song', 'dungeon'} and not ootworld.songs_as_items: # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. location = world.get_location('Sheik in Ice Cavern', player) - add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) + add_item_rule(location, lambda item: oot_is_item_of_type(item, 'Song')) + + if ootworld.shuffle_child_trade == 'skip_child_zelda': + # Song from Impa must be local + location = world.get_location('Song from Impa', player) + add_item_rule(location, lambda item: item.player == player) for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) @@ -176,11 +185,6 @@ def create_shop_rule(location, parser): return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price)) -def limit_to_itemset(location, itemset): - old_rule = location.item_rule - location.item_rule = lambda item: item.name in itemset and old_rule(item) - - # This function should be run once after the shop items are placed in the world. # It should be run before other items are placed in the world so that logic has # the correct checks for them. This is safe to do since every shop is still @@ -223,7 +227,8 @@ def set_shop_rules(ootworld): # The goal is to automatically set item rules based on age requirements in case entrances were shuffled def set_entrances_based_rules(ootworld): - all_state = ootworld.multiworld.get_all_state(False) + all_state = ootworld.get_state_with_complete_itempool() + all_state.sweep_for_events(locations=ootworld.get_locations()) for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()): # If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 6af19683f4..e9c889d6f6 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -43,14 +43,14 @@ i_o_limiter = threading.Semaphore(2) class OOTCollectionState(metaclass=AutoLogicRegister): def init_mixin(self, parent: MultiWorld): - all_ids = parent.get_all_ids() - self.child_reachable_regions = {player: set() for player in all_ids} - self.adult_reachable_regions = {player: set() for player in all_ids} - self.child_blocked_connections = {player: set() for player in all_ids} - self.adult_blocked_connections = {player: set() for player in all_ids} - self.day_reachable_regions = {player: set() for player in all_ids} - self.dampe_reachable_regions = {player: set() for player in all_ids} - self.age = {player: None for player in all_ids} + oot_ids = parent.get_game_players(OOTWorld.game) + parent.get_game_groups(OOTWorld.game) + self.child_reachable_regions = {player: set() for player in oot_ids} + self.adult_reachable_regions = {player: set() for player in oot_ids} + self.child_blocked_connections = {player: set() for player in oot_ids} + self.adult_blocked_connections = {player: set() for player in oot_ids} + self.day_reachable_regions = {player: set() for player in oot_ids} + self.dampe_reachable_regions = {player: set() for player in oot_ids} + self.age = {player: None for player in oot_ids} def copy_mixin(self, ret) -> CollectionState: ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in @@ -170,15 +170,19 @@ class OOTWorld(World): location_name_groups = build_location_name_groups() + def __init__(self, world, player): self.hint_data_available = threading.Event() self.collectible_flags_available = threading.Event() super(OOTWorld, self).__init__(world, player) + @classmethod def stage_assert_generate(cls, multiworld: MultiWorld): rom = Rom(file=get_options()['oot_options']['rom_file']) + + # Option parsing, handling incompatible options, building useful-item table def generate_early(self): self.parser = Rule_AST_Transformer(self, self.player) @@ -194,8 +198,10 @@ class OOTWorld(World): option_value = result.current_key setattr(self, option_name, option_value) + self.regions = [] # internal caches of regions for this world, used later + self._regions_cache = {} + self.shop_prices = {} - self.regions = [] # internal cache of regions for this world, used later self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory self.starting_items = Counter() self.songs_as_items = False @@ -489,6 +495,8 @@ class OOTWorld(World): # Farore's Wind skippable if not used for this logic trick in Water Temple self.nonadvancement_items.add('Farores Wind') + + # Reads a group of regions from the given JSON file. def load_regions_from_json(self, file_path): region_json = read_json(file_path) @@ -526,6 +534,10 @@ class OOTWorld(World): # We still need to fill the location even if ALR is off. logger.debug('Unreachable location: %s', new_location.name) new_location.player = self.player + # Change some attributes of Drop locations + if new_location.type == 'Drop': + new_location.name = new_region.name + ' ' + new_location.name + new_location.show_in_spoiler = False new_region.locations.append(new_location) if 'events' in region: for event, rule in region['events'].items(): @@ -555,8 +567,10 @@ class OOTWorld(World): self.multiworld.regions.append(new_region) self.regions.append(new_region) - self.multiworld._recache() + self._regions_cache[new_region.name] = new_region + + # Sets deku scrub prices def set_scrub_prices(self): # Get Deku Scrub Locations scrub_locations = [location for location in self.get_locations() if location.type in {'Scrub', 'GrottoScrub'}] @@ -585,6 +599,8 @@ class OOTWorld(World): if location.item is not None: location.item.price = price + + # Sets prices for shuffled shop locations def random_shop_prices(self): shop_item_indexes = ['7', '5', '8', '6'] self.shop_prices = {} @@ -610,6 +626,8 @@ class OOTWorld(World): elif self.shopsanity_prices == 'tycoons_wallet': self.shop_prices[location.name] = self.multiworld.random.randrange(0,1000,5) + + # Fill boss prizes def fill_bosses(self, bossCount=9): boss_location_names = ( 'Queen Gohma', @@ -622,7 +640,7 @@ class OOTWorld(World): 'Twinrova', 'Links Pocket' ) - boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward'] + boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards'])) boss_locations = [self.multiworld.get_location(loc, self.player) for loc in boss_location_names] placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None] @@ -636,9 +654,46 @@ class OOTWorld(World): item = prizepool.pop() loc = prize_locs.pop() loc.place_locked_item(item) - self.multiworld.itempool.remove(item) self.hinted_dungeon_reward_locations[item.name] = loc + + # Separate the result from generate_itempool into main and prefill pools + def divide_itempools(self): + prefill_item_types = set() + if self.shopsanity != 'off': + prefill_item_types.add('Shop') + if self.shuffle_song_items != 'any': + prefill_item_types.add('Song') + if self.shuffle_smallkeys != 'keysanity': + prefill_item_types.add('SmallKey') + if self.shuffle_bosskeys != 'keysanity': + prefill_item_types.add('BossKey') + if self.shuffle_hideoutkeys != 'keysanity': + prefill_item_types.add('HideoutSmallKey') + if self.shuffle_ganon_bosskey != 'keysanity': + prefill_item_types.add('GanonBossKey') + if self.shuffle_mapcompass != 'keysanity': + prefill_item_types.update({'Map', 'Compass'}) + + main_items = [] + prefill_items = [] + for item in self.itempool: + if item.type in prefill_item_types: + prefill_items.append(item) + else: + main_items.append(item) + return main_items, prefill_items + + + # only returns proper result after create_items and divide_itempools are run + def get_pre_fill_items(self): + return self.pre_fill_items + + + # Note on allow_arbitrary_name: + # OoT defines many helper items and event names that are treated indistinguishably from regular items, + # but are only defined in the logic files. This means we need to create items for any name. + # Allowing any item name to be created is dangerous in case of plando, so this is a middle ground. def create_item(self, name: str, allow_arbitrary_name: bool = False): if name in item_table: return OOTItem(name, self.player, item_table[name], False, @@ -658,7 +713,9 @@ class OOTWorld(World): location.internal = True return item - def create_regions(self): # create and link regions + + # Create regions, locations, and entrances + def create_regions(self): if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL world_type = 'World' else: @@ -671,7 +728,7 @@ class OOTWorld(World): self.multiworld.regions.append(menu) self.load_regions_from_json(overworld_data_path) self.load_regions_from_json(bosses_data_path) - start.connect(self.multiworld.get_region('Root', self.player)) + start.connect(self.get_region('Root')) create_dungeons(self) self.parser.create_delayed_rules() @@ -682,16 +739,13 @@ class OOTWorld(World): # Bind entrances to vanilla for region in self.regions: for exit in region.exits: - exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player)) + exit.connect(self.get_region(exit.vanilla_connected_region)) + + # Create items, starting item handling, boss prize fill (before entrance randomizer) def create_items(self): - # Uniquely rename drop locations for each region and erase them from the spoiler - set_drop_location_names(self) # Generate itempool generate_itempool(self) - # Add dungeon rewards - rewardlist = sorted(list(self.item_name_groups['rewards'])) - self.itempool += map(self.create_item, rewardlist) junk_pool = get_junk_pool(self) removed_items = [] @@ -714,12 +768,16 @@ class OOTWorld(World): if self.start_with_rupees: self.starting_items['Rupees'] = 999 + # Divide itempool into prefill and main pools + self.itempool, self.pre_fill_items = self.divide_itempools() + self.multiworld.itempool += self.itempool self.remove_from_start_inventory.extend(removed_items) # Fill boss prizes. needs to happen before entrance shuffle self.fill_bosses() + def set_rules(self): # This has to run AFTER creating items but BEFORE set_entrances_based_rules if self.entrance_shuffle: @@ -757,6 +815,7 @@ class OOTWorld(World): set_rules(self) set_entrances_based_rules(self) + def generate_basic(self): # mostly killing locations that shouldn't exist by settings # Gather items for ice trap appearances @@ -769,8 +828,9 @@ class OOTWorld(World): # Kill unreachable events that can't be gotten even with all items # Make sure to only kill actual internal events, not in-game "events" - all_state = self.multiworld.get_all_state(False) + all_state = self.get_state_with_complete_itempool() all_locations = self.get_locations() + all_state.sweep_for_events(locations=all_locations) reachable = self.multiworld.get_reachable_locations(all_state, self.player) unreachable = [loc for loc in all_locations if (loc.internal or loc.type == 'Drop') and loc.event and loc.locked and loc not in reachable] @@ -781,7 +841,6 @@ class OOTWorld(World): bigpoe = self.multiworld.get_location('Sell Big Poe from Market Guard House', self.player) if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable: bigpoe.parent_region.locations.remove(bigpoe) - self.multiworld.clear_location_cache() # If fast scarecrow then we need to kill the Pierre location as it will be unreachable if self.free_scarecrow: @@ -792,35 +851,63 @@ class OOTWorld(World): loc = self.multiworld.get_location("Deliver Rutos Letter", self.player) loc.parent_region.locations.remove(loc) + def pre_fill(self): + def prefill_state(base_state): + state = base_state.copy() + for item in self.get_pre_fill_items(): + self.collect(state, item) + state.sweep_for_events(locations=self.get_locations()) + return state + + # Prefill shops, songs, and dungeon items + items = self.get_pre_fill_items() + locations = list(self.multiworld.get_unfilled_locations(self.player)) + self.multiworld.random.shuffle(locations) + + # Set up initial state + state = CollectionState(self.multiworld) + for item in self.itempool: + self.collect(state, item) + state.sweep_for_events(locations=self.get_locations()) + # Place dungeon items special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - world_items = [item for item in self.multiworld.itempool if item.player == self.player] + type_to_setting = { + 'Map': 'shuffle_mapcompass', + 'Compass': 'shuffle_mapcompass', + 'SmallKey': 'shuffle_smallkeys', + 'BossKey': 'shuffle_bosskeys', + 'HideoutSmallKey': 'shuffle_hideoutkeys', + 'GanonBossKey': 'shuffle_ganon_bosskey', + } + special_fill_types.sort(key=lambda x: 0 if getattr(self, type_to_setting[x]) == 'dungeon' else 1) + for fill_stage in special_fill_types: - stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_items)) + stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items)) if not stage_items: continue if fill_stage in ['GanonBossKey', 'HideoutSmallKey']: locations = gather_locations(self.multiworld, fill_stage, self.player) if isinstance(locations, list): for item in stage_items: - self.multiworld.itempool.remove(item) + self.pre_fill_items.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, stage_items, + fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, single_player_placement=True, lock=True, allow_excluded=True) else: for dungeon_info in dungeon_table: dungeon_name = dungeon_info['name'] + dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) + if not dungeon_items: + continue locations = gather_locations(self.multiworld, fill_stage, self.player, dungeon=dungeon_name) if isinstance(locations, list): - dungeon_items = list(filter(lambda item: dungeon_name in item.name, stage_items)) - if not dungeon_items: - continue for item in dungeon_items: - self.multiworld.itempool.remove(item) + self.pre_fill_items.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, + fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) # Place songs @@ -836,9 +923,9 @@ class OOTWorld(World): else: raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}") - songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool)) + songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items)) for song in songs: - self.multiworld.itempool.remove(song) + self.pre_fill_items.remove(song) important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions) @@ -861,7 +948,7 @@ class OOTWorld(World): while tries: try: self.multiworld.random.shuffle(song_locations) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), song_locations[:], songs[:], + fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], single_player_placement=True, lock=True, allow_excluded=True) logger.debug(f"Successfully placed songs for player {self.player} after {6 - tries} attempt(s)") except FillError as e: @@ -883,10 +970,8 @@ class OOTWorld(World): # Place shop items # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items if self.shopsanity != 'off': - shop_prog = list(filter(lambda item: item.player == self.player and item.type == 'Shop' - and item.advancement, self.multiworld.itempool)) - shop_junk = list(filter(lambda item: item.player == self.player and item.type == 'Shop' - and not item.advancement, self.multiworld.itempool)) + shop_prog = list(filter(lambda item: item.type == 'Shop' and item.advancement, self.pre_fill_items)) + shop_junk = list(filter(lambda item: item.type == 'Shop' and not item.advancement, self.pre_fill_items)) shop_locations = list( filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, self.multiworld.get_unfilled_locations(player=self.player))) @@ -896,30 +981,14 @@ class OOTWorld(World): 'Buy Zora Tunic': 1, }.get(item.name, 0)) # place Deku Shields if needed, then tunics, then other advancement self.multiworld.random.shuffle(shop_locations) - for item in shop_prog + shop_junk: - self.multiworld.itempool.remove(item) - fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), shop_locations, shop_prog, + self.pre_fill_items = [] # all prefill should be done + fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, single_player_placement=True, lock=True, allow_excluded=True) fast_fill(self.multiworld, shop_junk, shop_locations) for loc in shop_locations: loc.locked = True set_shop_rules(self) # sets wallet requirements on shop items, must be done after they are filled - # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it. - impa = self.multiworld.get_location("Song from Impa", self.player) - if self.shuffle_child_trade == 'skip_child_zelda': - if impa.item is None: - candidate_items = list(item for item in self.multiworld.itempool if item.player == self.player) - if candidate_items: - item_to_place = self.multiworld.random.choice(candidate_items) - self.multiworld.itempool.remove(item_to_place) - else: - item_to_place = self.create_item("Recovery Heart") - impa.place_locked_item(item_to_place) - # Give items to startinventory - self.multiworld.push_precollected(impa.item) - self.multiworld.push_precollected(self.create_item("Zeldas Letter")) - # Exclude locations in Ganon's Castle proportional to the number of items required to make the bridge # Check for dungeon ER later if self.logic_rules == 'glitchless': @@ -954,48 +1023,6 @@ class OOTWorld(World): or (self.shuffle_child_trade == 'skip_child_zelda' and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None - # Handle item-linked dungeon items and songs - @classmethod - def stage_pre_fill(cls, multiworld: MultiWorld): - special_fill_types = ['Song', 'GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass'] - for group_id, group in multiworld.groups.items(): - if group['game'] != cls.game: - continue - group_items = [item for item in multiworld.itempool if item.player == group_id] - for fill_stage in special_fill_types: - group_stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), group_items)) - if not group_stage_items: - continue - if fill_stage in ['Song', 'GanonBossKey', 'HideoutSmallKey']: - # No need to subdivide by dungeon name - locations = gather_locations(multiworld, fill_stage, group['players']) - if isinstance(locations, list): - for item in group_stage_items: - multiworld.itempool.remove(item) - multiworld.random.shuffle(locations) - fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_stage_items, - single_player_placement=False, lock=True, allow_excluded=True) - if fill_stage == 'Song': - # We don't want song locations to contain progression unless it's a song - # or it was marked as priority. - # We do this manually because we'd otherwise have to either - # iterate twice or do many function calls. - for loc in locations: - if loc.progress_type == LocationProgressType.DEFAULT: - loc.progress_type = LocationProgressType.EXCLUDED - add_item_rule(loc, lambda i: not (i.advancement or i.useful)) - else: - # Perform the fill task once per dungeon - for dungeon_info in dungeon_table: - dungeon_name = dungeon_info['name'] - locations = gather_locations(multiworld, fill_stage, group['players'], dungeon=dungeon_name) - if isinstance(locations, list): - group_dungeon_items = list(filter(lambda item: dungeon_name in item.name, group_stage_items)) - for item in group_dungeon_items: - multiworld.itempool.remove(item) - multiworld.random.shuffle(locations) - fill_restrictive(multiworld, multiworld.get_all_state(False), locations, group_dungeon_items, - single_player_placement=False, lock=True, allow_excluded=True) def generate_output(self, output_directory: str): if self.hints != 'none': @@ -1032,30 +1059,6 @@ class OOTWorld(World): player_name=self.multiworld.get_player_name(self.player)) apz5.write() - # Write entrances to spoiler log - all_entrances = self.get_shuffled_entrances() - all_entrances.sort(reverse=True, key=lambda x: x.name) - all_entrances.sort(reverse=True, key=lambda x: x.type) - if not self.decouple_entrances: - while all_entrances: - loadzone = all_entrances.pop() - if loadzone.type != 'Overworld': - if loadzone.primary: - entrance = loadzone - else: - entrance = loadzone.reverse - if entrance.reverse is not None: - self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) - else: - self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) - else: - reverse = loadzone.replaces.reverse - if reverse in all_entrances: - all_entrances.remove(reverse) - self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player) - else: - for entrance in all_entrances: - self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations. @classmethod @@ -1135,6 +1138,7 @@ class OOTWorld(World): for autoworld in multiworld.get_game_worlds("Ocarina of Time"): autoworld.hint_data_available.set() + def fill_slot_data(self): self.collectible_flags_available.wait() return { @@ -1142,6 +1146,7 @@ class OOTWorld(World): 'collectible_flag_offsets': self.collectible_flag_offsets } + def modify_multidata(self, multidata: dict): # Replace connect name @@ -1156,6 +1161,16 @@ class OOTWorld(World): continue multidata["precollected_items"][self.player].remove(item_id) + # If skip child zelda, push item onto autotracker + if self.shuffle_child_trade == 'skip_child_zelda': + impa_item_id = self.item_name_to_id.get(self.get_location('Song from Impa').item.name, None) + zelda_item_id = self.item_name_to_id.get(self.get_location('HC Zeldas Letter').item.name, None) + if impa_item_id: + multidata["precollected_items"][self.player].append(impa_item_id) + if zelda_item_id: + multidata["precollected_items"][self.player].append(zelda_item_id) + + def extend_hint_information(self, er_hint_data: dict): er_hint_data[self.player] = {} @@ -1202,6 +1217,7 @@ class OOTWorld(World): er_hint_data[self.player][location.address] = main_entrance.name logger.debug(f"Set {location.name} hint data to {main_entrance.name}") + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: required_trials_str = ", ".join(t for t in self.skipped_trials if not self.skipped_trials[t]) spoiler_handle.write(f"\n\nTrials ({self.multiworld.get_player_name(self.player)}): {required_trials_str}\n") @@ -1211,6 +1227,32 @@ class OOTWorld(World): for k, v in self.shop_prices.items(): spoiler_handle.write(f"{k}: {v} Rupees\n") + # Write entrances to spoiler log + all_entrances = self.get_shuffled_entrances() + all_entrances.sort(reverse=True, key=lambda x: x.name) + all_entrances.sort(reverse=True, key=lambda x: x.type) + if not self.decouple_entrances: + while all_entrances: + loadzone = all_entrances.pop() + if loadzone.type != 'Overworld': + if loadzone.primary: + entrance = loadzone + else: + entrance = loadzone.reverse + if entrance.reverse is not None: + self.multiworld.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player) + else: + self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + else: + reverse = loadzone.replaces.reverse + if reverse in all_entrances: + all_entrances.remove(reverse) + self.multiworld.spoiler.set_entrance(loadzone, reverse, 'both', self.player) + else: + for entrance in all_entrances: + self.multiworld.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player) + + # Key ring handling: # Key rings are multiple items glued together into one, so we need to give # the appropriate number of keys in the collection state when they are @@ -1218,16 +1260,16 @@ class OOTWorld(World): def collect(self, state: CollectionState, item: OOTItem) -> bool: if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') - state.prog_items[alt_item_name, self.player] += count + state.prog_items[self.player][alt_item_name] += count return True return super().collect(state, item) def remove(self, state: CollectionState, item: OOTItem) -> bool: if item.advancement and item.special and item.special.get('alias', False): alt_item_name, count = item.special.get('alias') - state.prog_items[alt_item_name, self.player] -= count - if state.prog_items[alt_item_name, self.player] < 1: - del (state.prog_items[alt_item_name, self.player]) + state.prog_items[self.player][alt_item_name] -= count + if state.prog_items[self.player][alt_item_name] < 1: + del (state.prog_items[self.player][alt_item_name]) return True return super().remove(state, item) @@ -1242,24 +1284,29 @@ class OOTWorld(World): return False def get_shufflable_entrances(self, type=None, only_primary=False): - return [entrance for entrance in self.multiworld.get_entrances() if (entrance.player == self.player and - (type == None or entrance.type == type) and - (not only_primary or entrance.primary))] + return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type) + and (not only_primary or entrance.primary))] def get_shuffled_entrances(self, type=None, only_primary=False): return [entrance for entrance in self.get_shufflable_entrances(type=type, only_primary=only_primary) if entrance.shuffled] def get_locations(self): - for region in self.regions: - for loc in region.locations: - yield loc + return self.multiworld.get_locations(self.player) def get_location(self, location): return self.multiworld.get_location(location, self.player) - def get_region(self, region): - return self.multiworld.get_region(region, self.player) + def get_region(self, region_name): + try: + return self._regions_cache[region_name] + except KeyError: + ret = self.multiworld.get_region(region_name, self.player) + self._regions_cache[region_name] = ret + return ret + + def get_entrances(self): + return self.multiworld.get_entrances(self.player) def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) @@ -1294,9 +1341,8 @@ class OOTWorld(World): # In particular, ensures that Time Travel needs to be found. def get_state_with_complete_itempool(self): all_state = CollectionState(self.multiworld) - for item in self.multiworld.itempool: - if item.player == self.player: - self.multiworld.worlds[item.player].collect(all_state, item) + for item in self.itempool + self.pre_fill_items: + self.multiworld.worlds[item.player].collect(all_state, item) # If free_scarecrow give Scarecrow Song if self.free_scarecrow: all_state.collect(self.create_item("Scarecrow Song"), event=True) @@ -1336,7 +1382,6 @@ def gather_locations(multiworld: MultiWorld, dungeon: str = '' ) -> Optional[List[OOTLocation]]: type_to_setting = { - 'Song': 'shuffle_song_items', 'Map': 'shuffle_mapcompass', 'Compass': 'shuffle_mapcompass', 'SmallKey': 'shuffle_smallkeys', @@ -1355,21 +1400,12 @@ def gather_locations(multiworld: MultiWorld, players = {players} fill_opts = {p: getattr(multiworld.worlds[p], type_to_setting[item_type]) for p in players} locations = [] - if item_type == 'Song': - if any(map(lambda v: v == 'any', fill_opts.values())): - return None - for player, option in fill_opts.items(): - if option == 'song': - condition = lambda location: location.type == 'Song' - elif option == 'dungeon': - condition = lambda location: location.name in dungeon_song_locations - locations += filter(condition, multiworld.get_unfilled_locations(player=player)) - else: - if any(map(lambda v: v == 'keysanity', fill_opts.values())): - return None - for player, option in fill_opts.items(): - condition = functools.partial(valid_dungeon_item_location, - multiworld.worlds[player], option, dungeon) - locations += filter(condition, multiworld.get_unfilled_locations(player=player)) + if any(map(lambda v: v == 'keysanity', fill_opts.values())): + return None + for player, option in fill_opts.items(): + condition = functools.partial(valid_dungeon_item_location, + multiworld.worlds[player], option, dungeon) + locations += filter(condition, multiworld.get_unfilled_locations(player=player)) return locations + diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md index b4610878b6..fa8e148957 100644 --- a/worlds/oot/docs/en_Ocarina of Time.md +++ b/worlds/oot/docs/en_Ocarina of Time.md @@ -31,3 +31,10 @@ Items belonging to other worlds are represented by the Zelda's Letter item. When the player receives an item, Link will hold the item above his head and display it to the world. It's good for business! + +## Unique Local Commands + +The following commands are only available when using the OoTClient to play with Archipelago. + +- `/n64` Check N64 Connection State +- `/deathlink` Toggle deathlink from client. Overrides default setting. diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index 11aa737e0f..b2ee0702c9 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -445,13 +445,9 @@ class PokemonRedBlueWorld(World): # Delete evolution events for Pokémon that are not in logic in an all_state so that accessibility check does not # fail. Re-use test_state from previous final loop. evolutions_region = self.multiworld.get_region("Evolution", self.player) - clear_cache = False for location in evolutions_region.locations.copy(): if not test_state.can_reach(location, player=self.player): evolutions_region.locations.remove(location) - clear_cache = True - if clear_cache: - self.multiworld.clear_location_cache() if self.multiworld.old_man[self.player] == "early_parcel": self.multiworld.local_early_items[self.player]["Oak's Parcel"] = 1 @@ -467,13 +463,17 @@ class PokemonRedBlueWorld(World): locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - for loc in locs: + if not self.multiworld.key_items_only[self.player]: + rule = None if self.multiworld.fossil_check_item_types[self.player] == "key_items": - add_item_rule(loc, lambda i: i.advancement) + rule = lambda i: i.advancement elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": - add_item_rule(loc, lambda i: i.name in item_groups["Unique"]) + rule = lambda i: i.name in item_groups["Unique"] elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": - add_item_rule(loc, lambda i: not i.advancement) + rule = lambda i: not i.advancement + if rule: + for loc in locs: + add_item_rule(loc, rule) for mon in ([" ".join(self.multiworld.get_location( f"Oak's Lab - Starter {i}", self.player).item.name.split(" ")[1:]) for i in range(1, 4)] @@ -559,7 +559,6 @@ class PokemonRedBlueWorld(World): else: raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location") - self.multiworld._recache() if self.multiworld.door_shuffle[self.player] == "decoupled": swept_state = self.multiworld.state.copy() diff --git a/worlds/pokemon_rb/basepatch_blue.bsdiff4 b/worlds/pokemon_rb/basepatch_blue.bsdiff4 index b7bdda7fbbed37ff0fb9bff23d33460c38cdd1f3..eb4d83360cd854c12ec7f0983dd3944d6bfa7fd1 100644 GIT binary patch literal 45893 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

RP#AGhVah-Ib>@Av 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Τ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 literal 45511 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 diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index daefd6b2f7..086ec347f3 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -80,3 +80,9 @@ All items for other games will display simply as "AP ITEM," including those for A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is. If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to you until these have ended. + +## Unique Local Commands + +The following command is only available when using the PokemonClient to play with Archipelago. + +- `/gb` Check Gameboy Connection State diff --git a/worlds/pokemon_rb/rom.py b/worlds/pokemon_rb/rom.py index 4b191d9176..096ab8e0a1 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -546,10 +546,8 @@ def generate_output(self, output_directory: str): write_quizzes(self, data, random) - for location in self.multiworld.get_locations(): - if location.player != self.player: - continue - elif location.party_data: + for location in self.multiworld.get_locations(self.player): + if location.party_data: for party in location.party_data: if not isinstance(party["party_address"], list): addresses = [rom_addresses[party["party_address"]]] diff --git a/worlds/pokemon_rb/rom_addresses.py b/worlds/pokemon_rb/rom_addresses.py index 9c6621523c..97faf7bff2 100644 --- a/worlds/pokemon_rb/rom_addresses.py +++ b/worlds/pokemon_rb/rom_addresses.py @@ -1,10 +1,10 @@ rom_addresses = { "Option_Encounter_Minimum_Steps": 0x3c1, - "Option_Pitch_Black_Rock_Tunnel": 0x758, - "Option_Blind_Trainers": 0x30c3, - "Option_Trainersanity1": 0x3153, - "Option_Split_Card_Key": 0x3e0c, - "Option_Fix_Combat_Bugs": 0x3e0d, + "Option_Pitch_Black_Rock_Tunnel": 0x75c, + "Option_Blind_Trainers": 0x30c7, + "Option_Trainersanity1": 0x3157, + "Option_Split_Card_Key": 0x3e10, + "Option_Fix_Combat_Bugs": 0x3e11, "Option_Lose_Money": 0x40d4, "Base_Stats_Mew": 0x4260, "Title_Mon_First": 0x4373, diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index 0855e7a108..21dceb75e8 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -103,25 +103,25 @@ def set_rules(multiworld, player): "Route 22 - Trainer Parties": lambda state: state.has("Oak's Parcel", player), # # Rock Tunnel - # "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), - # "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - PokeManiac": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel 1F - Jr. Trainer F 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - PokeManiac 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - PokeManiac 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - PokeManiac 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Jr. Trainer F 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Jr. Trainer F 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Hiker 1": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Hiker 2": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Hiker 3": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - North Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Northwest Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - Southwest Item": lambda state: logic.rock_tunnel(state, player), + "Rock Tunnel B1F - West Item": lambda state: logic.rock_tunnel(state, player), # Pokédex check "Oak's Lab - Oak's Parcel Reward": lambda state: state.has("Oak's Parcel", player), diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 79739e85ef..0ed0a87b17 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -16,7 +16,7 @@ class Goal(Choice): display_name = "Game Mode" option_classic = 0 option_explore = 1 - default = 0 + default = 1 class TotalLocations(Range): @@ -48,7 +48,8 @@ class ScavengersPerEnvironment(Range): display_name = "Scavenger per Environment" range_start = 0 range_end = 1 - default = 1 + default = 0 + class ScannersPerEnvironment(Range): """Explore Mode: The number of scanners locations per environment.""" @@ -57,6 +58,7 @@ class ScannersPerEnvironment(Range): range_end = 1 default = 1 + class AltarsPerEnvironment(Range): """Explore Mode: The number of altars locations per environment.""" display_name = "Newts Per Environment" @@ -64,6 +66,7 @@ class AltarsPerEnvironment(Range): range_end = 2 default = 1 + class TotalRevivals(Range): """Total Percentage of `Dio's Best Friend` item put in the item pool.""" display_name = "Total Revives as percentage" @@ -83,6 +86,7 @@ class ItemPickupStep(Range): range_end = 5 default = 1 + class ShrineUseStep(Range): """ Explore Mode: @@ -131,7 +135,6 @@ class DLC_SOTV(Toggle): display_name = "Enable DLC - SOTV" - class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -274,25 +277,8 @@ class ItemWeights(Choice): option_void = 9 - - -# define a class for the weights of the generated item pool. @dataclass -class ROR2Weights: - green_scrap: GreenScrap - red_scrap: RedScrap - yellow_scrap: YellowScrap - white_scrap: WhiteScrap - common_item: CommonItem - uncommon_item: UncommonItem - legendary_item: LegendaryItem - boss_item: BossItem - lunar_item: LunarItem - void_item: VoidItem - equipment: Equipment - -@dataclass -class ROR2Options(PerGameCommonOptions, ROR2Weights): +class ROR2Options(PerGameCommonOptions): goal: Goal total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment @@ -310,4 +296,16 @@ class ROR2Options(PerGameCommonOptions, ROR2Weights): shrine_use_step: ShrineUseStep enable_lunar: AllowLunarItems item_weights: ItemWeights - item_pool_presets: ItemPoolPresetToggle \ No newline at end of file + item_pool_presets: ItemPoolPresetToggle + # define the weights of the generated item pool. + green_scrap: GreenScrap + red_scrap: RedScrap + yellow_scrap: YellowScrap + white_scrap: WhiteScrap + common_item: CommonItem + uncommon_item: UncommonItem + legendary_item: LegendaryItem + boss_item: BossItem + lunar_item: LunarItem + void_item: VoidItem + equipment: Equipment diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 7d94177417..65c04d06cb 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -96,8 +96,7 @@ def set_rules(multiworld: MultiWorld, player: int) -> None: # a long enough run to have enough director credits for scavengers and # help prevent being stuck in the same stages until that point.) - for location in multiworld.get_locations(): - if location.player != player: continue # ignore all checks that don't belong to this player + for location in multiworld.get_locations(player): if "Scavenger" in location.name: add_rule(location, lambda state: state.has("Stage_5", player)) # Regions diff --git a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md index f7c8519a2a..18bda64784 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -31,4 +31,24 @@ The goal is to beat the final mission: 'All In'. The config file determines whic By default, any of StarCraft 2's items (specified above) can be in another player's world. See the [Advanced YAML Guide](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) -for more information on how to change this. \ No newline at end of file +for more information on how to change this. + +## Unique Local Commands + +The following commands are only available when using the Starcraft 2 Client to play with Archipelago. + +- `/difficulty [difficulty]` Overrides the difficulty set for the world. + - Options: casual, normal, hard, brutal +- `/game_speed [game_speed]` Overrides the game speed for the world + - Options: default, slower, slow, normal, fast, faster +- `/color [color]` Changes your color (Currently has no effect) + - Options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen, brown, + lightgreen, darkgrey, pink, rainbow, random, default +- `/disable_mission_check` Disables the check to see if a mission is available to play. Meant for co-op runs where one + player can play the next mission in a chain the other player is doing. +- `/play [mission_id]` Starts a Starcraft 2 mission based off of the mission_id provided +- `/available` Get what missions are currently available to play +- `/unfinished` Get what missions are currently available to play and have not had all locations checked +- `/set_path [path]` Menually set the SC2 install directory (if the automatic detection fails) +- `/download_data` Download the most recent release of the necassry files for playing SC2 with Archipelago. Will + overwrite existing files diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index f208e600b9..3e9015eab7 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -112,15 +112,12 @@ class SMWorld(World): required_client_version = (0, 2, 6) itemManager: ItemManager - spheres = None Logic.factory('vanilla') def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() self.locations = {} - if SMWorld.spheres != None: - SMWorld.spheres = None super().__init__(world, player) @classmethod @@ -294,7 +291,7 @@ class SMWorld(World): for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions: src_region = self.multiworld.get_region(src.Name, self.player) dest_region = self.multiworld.get_region(dest.Name, self.player) - if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache): + if src.Name + "->" + dest.Name not in self.multiworld.regions.entrance_cache[self.player]: src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region)) srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player) srcDestEntrance.connect(dest_region) @@ -368,7 +365,7 @@ class SMWorld(World): locationsDict[first_local_collected_loc.name]), itemLoc.item.player, True) - for itemLoc in SMWorld.spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement) + for itemLoc in spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement) ] # Having a sorted itemLocs from collection order is required for escapeTrigger when Tourian is Disabled. @@ -376,8 +373,10 @@ class SMWorld(World): # get_spheres could be cached in multiworld? # Another possible solution would be to have a globally accessible list of items in the order in which the get placed in push_item # and use the inversed starting from the first progression item. - if (SMWorld.spheres == None): - SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)] + spheres: List[Location] = getattr(self.multiworld, "_sm_spheres", None) + if spheres is None: + spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)] + setattr(self.multiworld, "_sm_spheres", spheres) self.itemLocs = [ ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type @@ -390,7 +389,7 @@ class SMWorld(World): escapeTrigger = None if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"]: #used to simulate received items - first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player) + first_local_collected_loc = next(itemLoc for itemLoc in spheres if itemLoc.player == self.player) playerItemsItemLocs = get_player_ItemLocation(False) playerProgItemsItemLocs = get_player_ItemLocation(True) @@ -563,8 +562,8 @@ class SMWorld(World): multiWorldItems: List[ByteEdit] = [] idx = 0 vanillaItemTypesCount = 21 - for itemLoc in self.multiworld.get_locations(): - if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class: + for itemLoc in self.multiworld.get_locations(self.player): + if "Boss" not in locationsDict[itemLoc.name].Class: SMZ3NameToSMType = { "ETank": "ETank", "Missile": "Missile", "Super": "Super", "PowerBomb": "PowerBomb", "Bombs": "Bomb", "Charge": "Charge", "Ice": "Ice", "HiJump": "HiJump", "SpeedBooster": "SpeedBooster", diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index a603b61c58..8a10f3edea 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -88,6 +88,12 @@ class ExclamationBoxes(Choice): option_Off = 0 option_1Ups_Only = 1 +class CompletionType(Choice): + """Set goal for game completion""" + display_name = "Completion Goal" + option_Last_Bowser_Stage = 0 + option_All_Bowser_Stages = 1 + class ProgressiveKeys(DefaultOnToggle): """Keys will first grant you access to the Basement, then to the Secound Floor""" @@ -110,4 +116,5 @@ sm64_options: typing.Dict[str, type(Option)] = { "death_link": DeathLink, "BuddyChecks": BuddyChecks, "ExclamationBoxes": ExclamationBoxes, + "CompletionType" : CompletionType, } diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index 7c50ba4708..27b5fc8f7e 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -124,4 +124,9 @@ def set_rules(world, player: int, area_connections): add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value)) add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value)) - world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) + if world.CompletionType[player] == "last_bowser_stage": + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) + elif world.CompletionType[player] == "all_bowser_stages": + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \ + state.can_reach("Bowser in the Fire Sea", 'Region', player) and \ + state.can_reach("Bowser in the Sky", 'Region', player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 6a7a3bd272..3cc87708e7 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -154,6 +154,7 @@ class SM64World(World): "MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value, "StarsToFinish": self.multiworld.StarsToFinish[self.player].value, "DeathLink": self.multiworld.death_link[self.player].value, + "CompletionType" : self.multiworld.CompletionType[self.player].value, } def generate_output(self, output_directory: str): diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index e2eb2ac80a..2cc2ac97d9 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -470,7 +470,7 @@ class SMZ3World(World): def collect(self, state: CollectionState, item: Item) -> bool: state.smz3state[self.player].Add([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) if item.advancement: - state.prog_items[item.name, item.player] += 1 + state.prog_items[item.player][item.name] += 1 return True # indicate that a logical state change has occured return False @@ -478,9 +478,9 @@ class SMZ3World(World): name = self.collect_item(state, item, True) if name: state.smz3state[item.player].Remove([TotalSMZ3Item.Item(TotalSMZ3Item.ItemType[item.name], self.smz3World if hasattr(self, "smz3World") else None)]) - state.prog_items[name, item.player] -= 1 - if state.prog_items[name, item.player] < 1: - del (state.prog_items[name, item.player]) + state.prog_items[item.player][item.name] -= 1 + if state.prog_items[item.player][item.name] < 1: + del (state.prog_items[item.player][item.name]) return True return False diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 9a8f38cdac..d02a8d02ee 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -417,7 +417,7 @@ class SoEWorld(World): flags += option.to_flag() with open(placement_file, "wb") as f: # generate placement file - for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()): + for location in self.multiworld.get_locations(self.player): item = location.item assert item is not None, "Can't handle unfilled location" if item.code is None or location.address is None: diff --git a/worlds/stardew_valley/mods/mod_data.py b/worlds/stardew_valley/mods/mod_data.py index 81c4989411..30fe96c9d9 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -21,3 +21,11 @@ class ModNames: ayeisha = "Ayeisha - The Postal Worker (Custom NPC)" riley = "Custom NPC - Riley" skull_cavern_elevator = "Skull Cavern Elevator" + + +all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py index 5455a40e7a..9c96de00d3 100644 --- a/worlds/stardew_valley/stardew_rule.py +++ b/worlds/stardew_valley/stardew_rule.py @@ -88,6 +88,7 @@ assert true_ is True_() class Or(StardewRule): rules: FrozenSet[StardewRule] + _simplified: bool def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): rules_list: Set[StardewRule] @@ -112,6 +113,7 @@ class Or(StardewRule): rules_list = new_rules self.rules = frozenset(rules_list) + self._simplified = False def __call__(self, state: CollectionState) -> bool: return any(rule(state) for rule in self.rules) @@ -139,6 +141,8 @@ class Or(StardewRule): return min(rule.get_difficulty() for rule in self.rules) def simplify(self) -> StardewRule: + if self._simplified: + return self if true_ in self.rules: return true_ @@ -151,11 +155,14 @@ class Or(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - return Or(simplified_rules) + self.rules = frozenset(simplified_rules) + self._simplified = True + return self class And(StardewRule): rules: FrozenSet[StardewRule] + _simplified: bool def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): rules_list: Set[StardewRule] @@ -180,6 +187,7 @@ class And(StardewRule): rules_list = new_rules self.rules = frozenset(rules_list) + self._simplified = False def __call__(self, state: CollectionState) -> bool: return all(rule(state) for rule in self.rules) @@ -207,6 +215,8 @@ class And(StardewRule): return max(rule.get_difficulty() for rule in self.rules) def simplify(self) -> StardewRule: + if self._simplified: + return self if false_ in self.rules: return false_ @@ -219,7 +229,9 @@ class And(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - return And(simplified_rules) + self.rules = frozenset(simplified_rules) + self._simplified = True + return self class Count(StardewRule): diff --git a/worlds/stardew_valley/test/TestBackpack.py b/worlds/stardew_valley/test/TestBackpack.py index f26a7c1f03..378c90e40a 100644 --- a/worlds/stardew_valley/test/TestBackpack.py +++ b/worlds/stardew_valley/test/TestBackpack.py @@ -5,40 +5,41 @@ from .. import options class TestBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla} - def test_no_backpack_in_pool(self): - item_names = {item.name for item in self.multiworld.get_items()} - self.assertNotIn("Progressive Backpack", item_names) + def test_no_backpack(self): + with self.subTest("no items"): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_names) - def test_no_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Large Pack", location_names) - self.assertNotIn("Deluxe Pack", location_names) + with self.subTest("no locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Large Pack", location_names) + self.assertNotIn("Deluxe Pack", location_names) class TestBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} - def test_backpack_is_in_pool_2_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 2) + def test_backpack(self): + with self.subTest(check="has items"): + item_names = [item.name for item in self.multiworld.get_items()] + self.assertEqual(item_names.count("Progressive Backpack"), 2) - def test_2_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) + with self.subTest(check="has locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Large Pack", location_names) + self.assertIn("Deluxe Pack", location_names) -class TestBackpackEarlyProgressive(SVTestBase): +class TestBackpackEarlyProgressive(TestBackpackProgressive): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive} - def test_backpack_is_in_pool_2_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 2) + @property + def run_default_tests(self) -> bool: + # EarlyProgressive is default + return False - def test_2_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) + def test_backpack(self): + super().test_backpack() - def test_progressive_backpack_is_in_early_pool(self): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + with self.subTest(check="is early"): + self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 0142ad0079..46c6685ad5 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,5 +1,8 @@ +import typing + from BaseClasses import ItemClassification, MultiWorld -from . import setup_solo_multiworld, SVTestBase +from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_with_mods, \ + allsanity_options_without_mods, minimal_locations_maximal_items from .. import locations, items, location_table, options from ..data.villagers_data import all_villagers_by_name, all_villagers_by_mod_by_name from ..items import items_by_group, Group @@ -7,11 +10,11 @@ from ..locations import LocationTags from ..mods.mod_data import ModNames -def get_real_locations(tester: SVTestBase, multiworld: MultiWorld): +def get_real_locations(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): return [location for location in multiworld.get_locations(tester.player) if not location.event] -def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld): +def get_real_location_names(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): return [location.name for location in multiworld.get_locations(tester.player) if not location.event] @@ -115,21 +118,6 @@ class TestNoGingerIslandItemGeneration(SVTestBase): self.assertTrue(count == 0 or count == 2) -class TestGivenProgressiveBackpack(SVTestBase): - options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} - - def test_when_generate_world_then_two_progressive_backpack_are_added(self): - self.assertEqual(self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")), 2) - - def test_when_generate_world_then_backpack_locations_are_added(self): - created_locations = {location.name for location in self.multiworld.get_locations(1)} - backpacks_exist = [location.name in created_locations - for location in locations.locations_by_tag[LocationTags.BACKPACK] - if location.name != "Premium Pack"] - all_exist = all(backpacks_exist) - self.assertTrue(all_exist) - - class TestRemixedMineRewards(SVTestBase): def test_when_generate_world_then_one_reward_is_added_per_chest(self): # assert self.world.create_item("Rusty Sword") in self.multiworld.itempool @@ -205,17 +193,17 @@ class TestLocationGeneration(SVTestBase): self.assertIn(location.name, location_table) -class TestLocationAndItemCount(SVTestBase): +class TestLocationAndItemCount(SVTestCase): def test_minimal_location_maximal_items_still_valid(self): - min_max_options = self.minimal_locations_maximal_items() + min_max_options = minimal_locations_maximal_items() multiworld = setup_solo_multiworld(min_max_options) valid_locations = get_real_locations(self, multiworld) self.assertGreaterEqual(len(valid_locations), len(multiworld.itempool)) def test_allsanity_without_mods_has_at_least_locations(self): expected_locations = 994 - allsanity_options = self.allsanity_options_without_mods() + allsanity_options = allsanity_options_without_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -228,7 +216,7 @@ class TestLocationAndItemCount(SVTestBase): def test_allsanity_with_mods_has_at_least_locations(self): expected_locations = 1246 - allsanity_options = self.allsanity_options_with_mods() + allsanity_options = allsanity_options_with_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -245,6 +233,11 @@ class TestFriendsanityNone(SVTestBase): options.Friendsanity.internal_name: options.Friendsanity.option_none, } + @property + def run_default_tests(self) -> bool: + # None is default + return False + def test_no_friendsanity_items(self): for item in self.multiworld.itempool: self.assertFalse(item.name.endswith(" <3")) @@ -416,6 +409,7 @@ class TestFriendsanityAllNpcsWithMarriage(SVTestBase): self.assertLessEqual(int(hearts), 10) +""" # Assuming math is correct if we check 2 points class TestFriendsanityAllNpcsWithMarriageHeartSize2(SVTestBase): options = { options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, @@ -528,6 +522,7 @@ class TestFriendsanityAllNpcsWithMarriageHeartSize4(SVTestBase): self.assertTrue(hearts == 4 or hearts == 8 or hearts == 12 or hearts == 14) else: self.assertTrue(hearts == 4 or hearts == 8 or hearts == 10) +""" class TestFriendsanityAllNpcsWithMarriageHeartSize5(SVTestBase): diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 7f48f9347c..38f59c7490 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -6,12 +6,12 @@ import random from typing import Set from BaseClasses import ItemClassification, MultiWorld -from . import setup_solo_multiworld, SVTestBase +from . import setup_solo_multiworld, SVTestCase, allsanity_options_without_mods from .. import ItemData, StardewValleyWorld from ..items import Group, item_table -class TestItems(SVTestBase): +class TestItems(SVTestCase): def test_can_create_item_of_resource_pack(self): item_name = "Resource Pack: 500 Money" @@ -46,7 +46,7 @@ class TestItems(SVTestBase): def test_correct_number_of_stardrops(self): seed = random.randrange(sys.maxsize) - allsanity_options = self.allsanity_options_without_mods() + allsanity_options = allsanity_options_without_mods() multiworld = setup_solo_multiworld(allsanity_options, seed=seed) stardrop_items = [item for item in multiworld.get_items() if "Stardrop" in item.name] self.assertEqual(len(stardrop_items), 5) diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 33b2428098..3f02643b83 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,56 +1,57 @@ +import unittest from .. import True_ from ..logic import Received, Has, False_, And, Or -def test_simplify_true_in_and(): - rules = { - "Wood": True_(), - "Rock": True_(), - } - summer = Received("Summer", 0, 1) - assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer +class TestSimplification(unittest.TestCase): + def test_simplify_true_in_and(self): + rules = { + "Wood": True_(), + "Rock": True_(), + } + summer = Received("Summer", 0, 1) + self.assertEqual((Has("Wood", rules) & summer & Has("Rock", rules)).simplify(), + summer) + def test_simplify_false_in_or(self): + rules = { + "Wood": False_(), + "Rock": False_(), + } + summer = Received("Summer", 0, 1) + self.assertEqual((Has("Wood", rules) | summer | Has("Rock", rules)).simplify(), + summer) -def test_simplify_false_in_or(): - rules = { - "Wood": False_(), - "Rock": False_(), - } - summer = Received("Summer", 0, 1) - assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer + def test_simplify_and_in_and(self): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Winter', 0, 1), Received('Spring', 0, 1))) + self.assertEqual(rule.simplify(), + And(Received('Summer', 0, 1), Received('Fall', 0, 1), + Received('Winter', 0, 1), Received('Spring', 0, 1))) + def test_simplify_duplicated_and(self): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Summer', 0, 1), Received('Fall', 0, 1))) + self.assertEqual(rule.simplify(), + And(Received('Summer', 0, 1), Received('Fall', 0, 1))) -def test_simplify_and_in_and(): - rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), - And(Received('Winter', 0, 1), Received('Spring', 0, 1))) - assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), - Received('Spring', 0, 1)) + def test_simplify_or_in_or(self): + rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Winter', 0, 1), Received('Spring', 0, 1))) + self.assertEqual(rule.simplify(), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), + Received('Spring', 0, 1))) + def test_simplify_duplicated_or(self): + rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) + self.assertEqual(rule.simplify(), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) -def test_simplify_duplicated_and(): - rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), - And(Received('Summer', 0, 1), Received('Fall', 0, 1))) - assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1)) + def test_simplify_true_in_or(self): + rule = Or(True_(), Received('Summer', 0, 1)) + self.assertEqual(rule.simplify(), True_()) - -def test_simplify_or_in_or(): - rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), - Or(Received('Winter', 0, 1), Received('Spring', 0, 1))) - assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), - Received('Spring', 0, 1)) - - -def test_simplify_duplicated_or(): - rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), - Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) - assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1)) - - -def test_simplify_true_in_or(): - rule = Or(True_(), Received('Summer', 0, 1)) - assert rule.simplify() == True_() - - -def test_simplify_false_in_and(): - rule = And(False_(), Received('Summer', 0, 1)) - assert rule.simplify() == False_() + def test_simplify_false_in_and(self): + rule = And(False_(), Received('Summer', 0, 1)) + self.assertEqual(rule.simplify(), False_()) diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 712aa300d5..02b1ebf643 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,10 +1,11 @@ import itertools +import unittest from random import random from typing import Dict from BaseClasses import ItemClassification, MultiWorld from Options import SpecialRange -from . import setup_solo_multiworld, SVTestBase +from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods from .. import StardewItem, items_by_group, Group, StardewValleyWorld from ..locations import locations_by_tag, LocationTags, location_table from ..options import ExcludeGingerIsland, ToolProgression, Goal, SeasonRandomization, TrapItems, SpecialOrderLocations, ArcadeMachineLocations @@ -17,21 +18,21 @@ SEASONS = {Season.spring, Season.summer, Season.fall, Season.winter} TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} -def assert_can_win(tester: SVTestBase, multiworld: MultiWorld): +def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state)) -def basic_checks(tester: SVTestBase, multiworld: MultiWorld): +def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items()) assert_can_win(tester, multiworld) non_event_locations = [location for location in multiworld.get_locations() if not location.event] tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) -def check_no_ginger_island(tester: SVTestBase, multiworld: MultiWorld): +def check_no_ginger_island(tester: unittest.TestCase, multiworld: MultiWorld): ginger_island_items = [item_data.name for item_data in items_by_group[Group.GINGER_ISLAND]] ginger_island_locations = [location_data.name for location_data in locations_by_tag[LocationTags.GINGER_ISLAND]] for item in multiworld.get_items(): @@ -48,9 +49,9 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestBase): +class TestGenerateDynamicOptions(SVTestCase): def test_given_special_range_when_generate_then_basic_checks(self): - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange): continue @@ -62,7 +63,7 @@ class TestGenerateDynamicOptions(SVTestBase): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not option.options: continue @@ -73,7 +74,7 @@ class TestGenerateDynamicOptions(SVTestBase): basic_checks(self, multiworld) -class TestGoal(SVTestBase): +class TestGoal(SVTestCase): def test_given_goal_when_generate_then_victory_is_in_correct_location(self): for goal, location in [("community_center", GoalName.community_center), ("grandpa_evaluation", GoalName.grandpa_evaluation), @@ -90,7 +91,7 @@ class TestGoal(SVTestBase): self.assertEqual(victory.name, location) -class TestSeasonRandomization(SVTestBase): +class TestSeasonRandomization(SVTestCase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): world_options = {SeasonRandomization.internal_name: SeasonRandomization.option_disabled} multi_world = setup_solo_multiworld(world_options) @@ -114,7 +115,7 @@ class TestSeasonRandomization(SVTestBase): self.assertEqual(items.count(Season.progressive), 3) -class TestToolProgression(SVTestBase): +class TestToolProgression(SVTestCase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): world_options = {ToolProgression.internal_name: ToolProgression.option_vanilla} multi_world = setup_solo_multiworld(world_options) @@ -147,9 +148,9 @@ class TestToolProgression(SVTestBase): self.assertIn("Purchase Iridium Rod", locations) -class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): +class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): def test_given_special_range_when_generate_exclude_ginger_island(self): - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: continue @@ -162,7 +163,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) - options = self.world.options_dataclass.type_hints + options = StardewValleyWorld.options_dataclass.type_hints for option_name, option in options.items(): if not option.options or option_name == ExcludeGingerIsland.internal_name: continue @@ -191,9 +192,9 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): basic_checks(self, multiworld) -class TestTraps(SVTestBase): +class TestTraps(SVTestCase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = self.allsanity_options_without_mods() + world_options = allsanity_options_without_mods() world_options.update({TrapItems.internal_name: TrapItems.option_no_traps}) multi_world = setup_solo_multiworld(world_options) @@ -209,7 +210,7 @@ class TestTraps(SVTestBase): for value in trap_option.options: if value == "no_traps": continue - world_options = self.allsanity_options_with_mods() + world_options = allsanity_options_with_mods() world_options.update({TrapItems.internal_name: trap_option.options[value]}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups and item_data.mod_name is None] @@ -219,7 +220,7 @@ class TestTraps(SVTestBase): self.assertIn(item, multiworld_items) -class TestSpecialOrders(SVTestBase): +class TestSpecialOrders(SVTestCase): def test_given_disabled_then_no_order_in_pool(self): world_options = {SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled} multi_world = setup_solo_multiworld(world_options) diff --git a/worlds/stardew_valley/test/TestRegions.py b/worlds/stardew_valley/test/TestRegions.py index 2347ca33db..7ebbcece5c 100644 --- a/worlds/stardew_valley/test/TestRegions.py +++ b/worlds/stardew_valley/test/TestRegions.py @@ -2,7 +2,7 @@ import random import sys import unittest -from . import SVTestBase, setup_solo_multiworld +from . import SVTestCase, setup_solo_multiworld from .. import options, StardewValleyWorld, StardewValleyOptions from ..options import EntranceRandomization, ExcludeGingerIsland from ..regions import vanilla_regions, vanilla_connections, randomize_connections, RandomizationFlag @@ -88,7 +88,7 @@ class TestEntranceRando(unittest.TestCase): f"Connections are duplicated in randomization. Seed = {seed}") -class TestEntranceClassifications(SVTestBase): +class TestEntranceClassifications(SVTestCase): def test_non_progression_are_all_accessible_with_empty_inventory(self): for option, flag in [(options.EntranceRandomization.option_pelican_town, RandomizationFlag.PELICAN_TOWN), diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 0847d8a63b..72337812cd 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -24,7 +24,7 @@ class TestProgressiveToolsLogic(SVTestBase): def setUp(self): super().setUp() - self.multiworld.state.prog_items = Counter() + self.multiworld.state.prog_items = {1: Counter()} def test_sturgeon(self): self.assertFalse(self.world.logic.has("Sturgeon")(self.multiworld.state)) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index 53181154d3..b0c4ba2c7b 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,8 +1,10 @@ import os +import unittest from argparse import Namespace from typing import Dict, FrozenSet, Tuple, Any, ClassVar from BaseClasses import MultiWorld +from Utils import cache_argsless from test.TestBase import WorldTestBase from test.general import gen_steps, setup_solo_multiworld as setup_base_solo_multiworld from .. import StardewValleyWorld @@ -13,11 +15,17 @@ from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Frien BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods -class SVTestBase(WorldTestBase): +class SVTestCase(unittest.TestCase): + player: ClassVar[int] = 1 + """Set to False to not skip some 'extra' tests""" + skip_extra_tests: bool = True + """Set to False to run tests that take long""" + skip_long_tests: bool = True + + +class SVTestBase(WorldTestBase, SVTestCase): game = "Stardew Valley" world: StardewValleyWorld - player: ClassVar[int] = 1 - skip_long_tests: bool = True def world_setup(self, *args, **kwargs): super().world_setup(*args, **kwargs) @@ -34,66 +42,73 @@ class SVTestBase(WorldTestBase): should_run_default_tests = is_not_stardew_test and super().run_default_tests return should_run_default_tests - def minimal_locations_maximal_items(self): - min_max_options = { - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, - BackpackProgression.internal_name: BackpackProgression.option_vanilla, - ToolProgression.internal_name: ToolProgression.option_vanilla, - SkillProgression.internal_name: SkillProgression.option_vanilla, - BuildingProgression.internal_name: BuildingProgression.option_vanilla, - ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, - HelpWantedLocations.internal_name: 0, - Fishsanity.internal_name: Fishsanity.option_none, - Museumsanity.internal_name: Museumsanity.option_none, - Friendsanity.internal_name: Friendsanity.option_none, - NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, - } - return min_max_options - def allsanity_options_without_mods(self): - allsanity = { - Goal.internal_name: Goal.option_perfection, - BundleRandomization.internal_name: BundleRandomization.option_shuffled, - BundlePrice.internal_name: BundlePrice.option_expensive, - SeasonRandomization.internal_name: SeasonRandomization.option_randomized, - Cropsanity.internal_name: Cropsanity.option_shuffled, - BackpackProgression.internal_name: BackpackProgression.option_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, - SkillProgression.internal_name: SkillProgression.option_progressive, - BuildingProgression.internal_name: BuildingProgression.option_progressive, - FestivalLocations.internal_name: FestivalLocations.option_hard, - ElevatorProgression.internal_name: ElevatorProgression.option_progressive, - ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, - SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, - HelpWantedLocations.internal_name: 56, - Fishsanity.internal_name: Fishsanity.option_all, - Museumsanity.internal_name: Museumsanity.option_all, - Friendsanity.internal_name: Friendsanity.option_all_with_marriage, - FriendsanityHeartSize.internal_name: 1, - NumberOfMovementBuffs.internal_name: 12, - NumberOfLuckBuffs.internal_name: 12, - ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, - TrapItems.internal_name: TrapItems.option_nightmare, - } - return allsanity +@cache_argsless +def minimal_locations_maximal_items(): + min_max_options = { + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_shuffled, + BackpackProgression.internal_name: BackpackProgression.option_vanilla, + ToolProgression.internal_name: ToolProgression.option_vanilla, + SkillProgression.internal_name: SkillProgression.option_vanilla, + BuildingProgression.internal_name: BuildingProgression.option_vanilla, + ElevatorProgression.internal_name: ElevatorProgression.option_vanilla, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_disabled, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_disabled, + HelpWantedLocations.internal_name: 0, + Fishsanity.internal_name: Fishsanity.option_none, + Museumsanity.internal_name: Museumsanity.option_none, + Friendsanity.internal_name: Friendsanity.option_none, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + } + return min_max_options + + +@cache_argsless +def allsanity_options_without_mods(): + allsanity = { + Goal.internal_name: Goal.option_perfection, + BundleRandomization.internal_name: BundleRandomization.option_shuffled, + BundlePrice.internal_name: BundlePrice.option_expensive, + SeasonRandomization.internal_name: SeasonRandomization.option_randomized, + Cropsanity.internal_name: Cropsanity.option_shuffled, + BackpackProgression.internal_name: BackpackProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive, + SkillProgression.internal_name: SkillProgression.option_progressive, + BuildingProgression.internal_name: BuildingProgression.option_progressive, + FestivalLocations.internal_name: FestivalLocations.option_hard, + ElevatorProgression.internal_name: ElevatorProgression.option_progressive, + ArcadeMachineLocations.internal_name: ArcadeMachineLocations.option_full_shuffling, + SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi, + HelpWantedLocations.internal_name: 56, + Fishsanity.internal_name: Fishsanity.option_all, + Museumsanity.internal_name: Museumsanity.option_all, + Friendsanity.internal_name: Friendsanity.option_all_with_marriage, + FriendsanityHeartSize.internal_name: 1, + NumberOfMovementBuffs.internal_name: 12, + NumberOfLuckBuffs.internal_name: 12, + ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false, + TrapItems.internal_name: TrapItems.option_nightmare, + } + return allsanity + + +@cache_argsless +def allsanity_options_with_mods(): + allsanity = {} + allsanity.update(allsanity_options_without_mods()) + all_mods = ( + ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, + ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, + ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, + ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, + ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, + ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator + ) + allsanity.update({Mods.internal_name: all_mods}) + return allsanity - def allsanity_options_with_mods(self): - allsanity = {} - allsanity.update(self.allsanity_options_without_mods()) - all_mods = ( - ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator - ) - allsanity.update({Mods.internal_name: all_mods}) - return allsanity pre_generated_worlds = {} diff --git a/worlds/stardew_valley/test/checks/world_checks.py b/worlds/stardew_valley/test/checks/world_checks.py index 2cdb0534d4..9bd9fd614c 100644 --- a/worlds/stardew_valley/test/checks/world_checks.py +++ b/worlds/stardew_valley/test/checks/world_checks.py @@ -1,8 +1,8 @@ +import unittest from typing import List from BaseClasses import MultiWorld, ItemClassification from ... import StardewItem -from .. import SVTestBase def get_all_item_names(multiworld: MultiWorld) -> List[str]: @@ -13,21 +13,21 @@ def get_all_location_names(multiworld: MultiWorld) -> List[str]: return [location.name for location in multiworld.get_locations() if not location.event] -def assert_victory_exists(tester: SVTestBase, multiworld: MultiWorld): +def assert_victory_exists(tester: unittest.TestCase, multiworld: MultiWorld): tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items()) -def collect_all_then_assert_can_win(tester: SVTestBase, multiworld: MultiWorld): +def collect_all_then_assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): for item in multiworld.get_items(): multiworld.state.collect(item) tester.assertTrue(multiworld.find_item("Victory", 1).can_reach(multiworld.state)) -def assert_can_win(tester: SVTestBase, multiworld: MultiWorld): +def assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): assert_victory_exists(tester, multiworld) collect_all_then_assert_can_win(tester, multiworld) -def assert_same_number_items_locations(tester: SVTestBase, multiworld: MultiWorld): +def assert_same_number_items_locations(tester: unittest.TestCase, multiworld: MultiWorld): non_event_locations = [location for location in multiworld.get_locations() if not location.event] tester.assertEqual(len(multiworld.itempool), len(non_event_locations)) \ No newline at end of file diff --git a/worlds/stardew_valley/test/long/TestModsLong.py b/worlds/stardew_valley/test/long/TestModsLong.py index b3ec6f1420..36a59ae854 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -1,23 +1,17 @@ +import unittest from typing import List, Union from BaseClasses import MultiWorld -from worlds.stardew_valley.mods.mod_data import ModNames +from worlds.stardew_valley.mods.mod_data import all_mods from worlds.stardew_valley.test import setup_solo_multiworld -from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase +from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestCase from worlds.stardew_valley.items import item_table from worlds.stardew_valley.locations import location_table from worlds.stardew_valley.options import Mods from .option_names import options_to_include -all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) - -def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -30,7 +24,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestBase): +class TestGenerateModsOptions(SVTestCase): def test_given_mod_pairs_when_generate_then_basic_checks(self): if self.skip_long_tests: diff --git a/worlds/stardew_valley/test/long/TestOptionsLong.py b/worlds/stardew_valley/test/long/TestOptionsLong.py index 23ac6125e6..3634dc5fd1 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,13 +1,14 @@ +import unittest from typing import Dict from BaseClasses import MultiWorld from Options import SpecialRange from .option_names import options_to_include from worlds.stardew_valley.test.checks.world_checks import assert_can_win, assert_same_number_items_locations -from .. import setup_solo_multiworld, SVTestBase +from .. import setup_solo_multiworld, SVTestCase -def basic_checks(tester: SVTestBase, multiworld: MultiWorld): +def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): assert_can_win(tester, multiworld) assert_same_number_items_locations(tester, multiworld) @@ -20,7 +21,7 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestBase): +class TestGenerateDynamicOptions(SVTestCase): def test_given_option_pair_when_generate_then_basic_checks(self): if self.skip_long_tests: return diff --git a/worlds/stardew_valley/test/long/TestRandomWorlds.py b/worlds/stardew_valley/test/long/TestRandomWorlds.py index 0145f471d1..e22c6c3564 100644 --- a/worlds/stardew_valley/test/long/TestRandomWorlds.py +++ b/worlds/stardew_valley/test/long/TestRandomWorlds.py @@ -4,7 +4,7 @@ import random from BaseClasses import MultiWorld from Options import SpecialRange, Range from .option_names import options_to_include -from .. import setup_solo_multiworld, SVTestBase +from .. import setup_solo_multiworld, SVTestCase from ..checks.goal_checks import assert_perfection_world_is_valid, assert_goal_world_is_valid from ..checks.option_checks import assert_can_reach_island_if_should, assert_cropsanity_same_number_items_and_locations, \ assert_festivals_give_access_to_deluxe_scarecrow @@ -72,14 +72,14 @@ def generate_many_worlds(number_worlds: int, start_index: int) -> Dict[int, Mult return multiworlds -def check_every_multiworld_is_valid(tester: SVTestBase, multiworlds: Dict[int, MultiWorld]): +def check_every_multiworld_is_valid(tester: SVTestCase, multiworlds: Dict[int, MultiWorld]): for multiworld_id in multiworlds: multiworld = multiworlds[multiworld_id] with tester.subTest(f"Checking validity of world {multiworld_id}"): check_multiworld_is_valid(tester, multiworld_id, multiworld) -def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld: MultiWorld): +def check_multiworld_is_valid(tester: SVTestCase, multiworld_id: int, multiworld: MultiWorld): assert_victory_exists(tester, multiworld) assert_same_number_items_locations(tester, multiworld) assert_goal_world_is_valid(tester, multiworld) @@ -88,7 +88,7 @@ def check_multiworld_is_valid(tester: SVTestBase, multiworld_id: int, multiworld assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld) -class TestGenerateManyWorlds(SVTestBase): +class TestGenerateManyWorlds(SVTestCase): def test_generate_many_worlds_then_check_results(self): if self.skip_long_tests: return diff --git a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py index 0265f61731..bc81f21963 100644 --- a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py +++ b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py @@ -7,45 +7,40 @@ class TestBiggerBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, options.Mods.internal_name: ModNames.big_backpack} - def test_no_backpack_in_pool(self): - item_names = {item.name for item in self.multiworld.get_items()} - self.assertNotIn("Progressive Backpack", item_names) + def test_no_backpack(self): + with self.subTest(check="no items"): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_names) - def test_no_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertNotIn("Large Pack", location_names) - self.assertNotIn("Deluxe Pack", location_names) - self.assertNotIn("Premium Pack", location_names) + with self.subTest(check="no locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertNotIn("Large Pack", location_names) + self.assertNotIn("Deluxe Pack", location_names) + self.assertNotIn("Premium Pack", location_names) class TestBiggerBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, options.Mods.internal_name: ModNames.big_backpack} - def test_backpack_is_in_pool_3_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 3) + def test_backpack(self): + with self.subTest(check="has items"): + item_names = [item.name for item in self.multiworld.get_items()] + self.assertEqual(item_names.count("Progressive Backpack"), 3) - def test_3_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) - self.assertIn("Premium Pack", location_names) + with self.subTest(check="has locations"): + location_names = {location.name for location in self.multiworld.get_locations()} + self.assertIn("Large Pack", location_names) + self.assertIn("Deluxe Pack", location_names) + self.assertIn("Premium Pack", location_names) -class TestBiggerBackpackEarlyProgressive(SVTestBase): +class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, options.Mods.internal_name: ModNames.big_backpack} - def test_backpack_is_in_pool_3_times(self): - item_names = [item.name for item in self.multiworld.get_items()] - self.assertEqual(item_names.count("Progressive Backpack"), 3) + def test_backpack(self): + super().test_backpack() - def test_3_backpack_locations(self): - location_names = {location.name for location in self.multiworld.get_locations()} - self.assertIn("Large Pack", location_names) - self.assertIn("Deluxe Pack", location_names) - self.assertIn("Premium Pack", location_names) - - def test_progressive_backpack_is_in_early_pool(self): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + with self.subTest(check="is early"): + self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 02fd30a6b1..9bdabaf73f 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,24 +4,17 @@ import random import sys from BaseClasses import MultiWorld -from ...mods.mod_data import ModNames -from .. import setup_solo_multiworld -from ..TestOptions import basic_checks, SVTestBase +from ...mods.mod_data import all_mods +from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods +from ..TestOptions import basic_checks from ... import items, Group, ItemClassification from ...regions import RandomizationFlag, create_final_connections, randomize_connections, create_final_regions from ...items import item_table, items_by_group from ...locations import location_table from ...options import Mods, EntranceRandomization, Friendsanity, SeasonRandomization, SpecialOrderLocations, ExcludeGingerIsland, TrapItems -all_mods = frozenset({ModNames.deepwoods, ModNames.tractor, ModNames.big_backpack, - ModNames.luck_skill, ModNames.magic, ModNames.socializing_skill, ModNames.archaeology, - ModNames.cooking_skill, ModNames.binning_skill, ModNames.juna, - ModNames.jasper, ModNames.alec, ModNames.yoba, ModNames.eugene, - ModNames.wellwick, ModNames.ginger, ModNames.shiko, ModNames.delores, - ModNames.ayeisha, ModNames.riley, ModNames.skull_cavern_elevator}) - -def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.TestCase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -34,7 +27,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestBase): +class TestGenerateModsOptions(SVTestCase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in all_mods: @@ -50,6 +43,8 @@ class TestGenerateModsOptions(SVTestBase): multiworld = setup_solo_multiworld({EntranceRandomization.internal_name: option, Mods: mod}) basic_checks(self, multiworld) check_stray_mod_items(mod, self, multiworld) + if self.skip_extra_tests: + return # assume the rest will work as well class TestBaseItemGeneration(SVTestBase): @@ -103,7 +98,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase): self.assertIn(progression_item.name, all_created_items) -class TestModEntranceRando(unittest.TestCase): +class TestModEntranceRando(SVTestCase): def test_mod_entrance_randomization(self): @@ -137,12 +132,12 @@ class TestModEntranceRando(unittest.TestCase): f"Connections are duplicated in randomization. Seed = {seed}") -class TestModTraps(SVTestBase): +class TestModTraps(SVTestCase): def test_given_traps_when_generate_then_all_traps_in_pool(self): for value in TrapItems.options: if value == "no_traps": continue - world_options = self.allsanity_options_without_mods() + world_options = allsanity_options_without_mods() world_options.update({TrapItems.internal_name: TrapItems.options[value], Mods: "Magic"}) multi_world = setup_solo_multiworld(world_options) trap_items = [item_data.name for item_data in items_by_group[Group.TRAP] if Group.DEPRECATED not in item_data.groups] diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md index 84744a4a33..b69af591fa 100644 --- a/worlds/terraria/docs/setup_en.md +++ b/worlds/terraria/docs/setup_en.md @@ -31,6 +31,8 @@ highly recommended to use utility mods and features to speed up gameplay, such a - (Can be used to break progression) - Reduced Grinding - Upgraded Research + - (WARNING: Do not use without Journey mode) + - (NOTE: If items you pick up aren't showing up in your inventory, check your research menu. This mod automatically researches certain items.) ## Configuring your YAML File diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md index e443c9b953..7c2e6deda5 100644 --- a/worlds/tloz/docs/en_The Legend of Zelda.md +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -35,9 +35,17 @@ filler and useful items will cost less, and uncategorized items will be in the m ## Are there any other changes made? -- The map and compass for each dungeon start already acquired, and other items can be found in their place. +- The map and compass for each dungeon start already acquired, and other items can be found in their place. - The Recorder will warp you between all eight levels regardless of Triforce count - - It's possible for this to be your route to level 4! + - It's possible for this to be your route to level 4! - Pressing Select will cycle through your inventory. - Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position. -- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file +- What slots from a Take Any Cave have been chosen are similarly tracked. +- + +## Local Unique Commands + +The following commands are only available when using the Zelda1Client to play with Archipelago. + +- `/nes` Check NES Connection State +- `/toggle_msgs` Toggle displaying messages in EmuHawk diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index ae53d953b1..df857f16df 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -6,6 +6,7 @@ - Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) - The BizHawk emulator. Versions 2.3.1 and higher are supported. - [BizHawk at TASVideos](https://tasvideos.org/BizHawk) +- Your legally acquired US v1.0 PRG0 ROM file, probably named `Legend of Zelda, The (U) (PRG0) [!].nes` ## Optional Software diff --git a/worlds/undertale/__init__.py b/worlds/undertale/__init__.py index 5e36344703..9e784a4a59 100644 --- a/worlds/undertale/__init__.py +++ b/worlds/undertale/__init__.py @@ -193,7 +193,7 @@ class UndertaleWorld(World): def create_regions(self): def UndertaleRegion(region_name: str, exits=[]): ret = Region(region_name, self.player, self.multiworld) - ret.locations = [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret) + ret.locations += [UndertaleAdvancement(self.player, loc_name, loc_data.id, ret) for loc_name, loc_data in advancement_table.items() if loc_data.region == region_name and (loc_name not in exclusion_table["NoStats"] or diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md index 3905d3bc3e..87011ee16b 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -42,11 +42,22 @@ In the Pacifist run, you are not required to go to the Ruins to spare Toriel. Th Undyne, and Mettaton EX. Just as it is in the vanilla game, you cannot kill anyone. You are also required to complete the date/hangout with Papyrus, Undyne, and Alphys, in that order, before entering the True Lab. -Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight -Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`, +Additionally, custom items are required to hang out with Papyrus, Undyne, to enter the True Lab, and to fight +Mettaton EX/NEO. The respective items for each interaction are `Complete Skeleton`, `Fish`, `DT Extractor`, and `Mettaton Plush`. -The Riverperson will only take you to locations you have seen them at, meaning they will only take you to +The Riverperson will only take you to locations you have seen them at, meaning they will only take you to Waterfall if you have seen them at Waterfall at least once. -If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas. \ No newline at end of file +If you press `W` while in the save menu, you will teleport back to the flower room, for quick access to the other areas. + +## Unique Local Commands + +The following commands are only available when using the UndertaleClient to play with Archipelago. + +- `/resync` Manually trigger a resync. +- `/patch` Patch the game. +- `/savepath` Redirect to proper save data folder. (Use before connecting!) +- `/auto_patch` Patch the game automatically. +- `/online` Makes you no longer able to see other Undertale players. +- `/deathlink` Toggles deathlink diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md index 18474a4269..f08902535d 100644 --- a/worlds/wargroove/docs/en_Wargroove.md +++ b/worlds/wargroove/docs/en_Wargroove.md @@ -26,9 +26,16 @@ Any of the above items can be in another player's world. ## When the player receives an item, what happens? -When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action +When the player receives an item, a message will appear in Wargroove with the item name and sender name, once an action is taken in game. ## What is the goal of this game when randomized? The goal is to beat the level titled `The End` by finding the `Final Bridges`, `Final Walls`, and `Final Sickle`. + +## Unique Local Commands + +The following commands are only available when using the WargrooveClient to play with Archipelago. + +- `/resync` Manually trigger a resync. +- `/commander` Set the current commander to the given commander. diff --git a/worlds/witness/hints.py b/worlds/witness/hints.py index 4fd0edc429..8a9dab54bc 100644 --- a/worlds/witness/hints.py +++ b/worlds/witness/hints.py @@ -228,8 +228,8 @@ def make_hints(multiworld: MultiWorld, player: int, hint_amount: int): if item.player == player and item.code and item.advancement } loc_in_this_world = { - location.name for location in multiworld.get_locations() - if location.player == player and location.address + location.name for location in multiworld.get_locations(player) + if location.address } always_locations = [ diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 1e79f4f133..a5e1bfe1ad 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -329,23 +329,22 @@ class ZillionWorld(World): empty = zz_items[4] multi_item = empty # a different patcher method differentiates empty from ap multi item multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name) - for loc in self.multiworld.get_locations(): - if loc.player == self.player: - z_loc = cast(ZillionLocation, loc) - # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) - if z_loc.item is None: - self.logger.warn("generate_output location has no item - is that ok?") - z_loc.zz_loc.item = empty - elif z_loc.item.player == self.player: - z_item = cast(ZillionItem, z_loc.item) - z_loc.zz_loc.item = z_item.zz_item - else: # another player's item - # print(f"put multi item in {z_loc.zz_loc.name}") - z_loc.zz_loc.item = multi_item - multi_items[z_loc.zz_loc.name] = ( - z_loc.item.name, - self.multiworld.get_player_name(z_loc.item.player) - ) + for loc in self.multiworld.get_locations(self.player): + z_loc = cast(ZillionLocation, loc) + # debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc) + if z_loc.item is None: + self.logger.warn("generate_output location has no item - is that ok?") + z_loc.zz_loc.item = empty + elif z_loc.item.player == self.player: + z_item = cast(ZillionItem, z_loc.item) + z_loc.zz_loc.item = z_item.zz_item + else: # another player's item + # print(f"put multi item in {z_loc.zz_loc.name}") + z_loc.zz_loc.item = multi_item + multi_items[z_loc.zz_loc.name] = ( + z_loc.item.name, + self.multiworld.get_player_name(z_loc.item.player) + ) # debug_zz_loc_ids.sort() # for name, id_ in debug_zz_loc_ids.items(): # print(id_) diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md index b5d37cc202..06a11b7d79 100644 --- a/worlds/zillion/docs/en_Zillion.md +++ b/worlds/zillion/docs/en_Zillion.md @@ -67,8 +67,16 @@ Note that in "restrictive" mode, Champ is the only one that can get Zillion powe Canisters retain their original appearance, so you won't know if an item belongs to another player until you collect it. -When you collect an item, you see the name of the player it goes to. You can see in the client log what item was collected. +When you collect an item, you see the name of the player it goes to. You can see in the client log what item was +collected. ## When the player receives an item, what happens? The item collect sound is played. You can see in the client log what item was received. + +## Unique Local Commands + +The following commands are only available when using the ZillionClient to play with Archipelago. + +- `/sms` Tell the client that Zillion is running in RetroArch. +- `/map` Toggle view of the map tracker.