From a028b9aac380d8e8959fbfac042b4f73b33ee270 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 27 Oct 2023 14:15:28 -0400 Subject: [PATCH 1/8] Snatcher Coins fix --- worlds/ahit/Options.py | 16 ++++++++++++++++ worlds/ahit/Regions.py | 13 ++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 863f940f0a..f3dd2a8c66 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -28,6 +28,22 @@ def adjust_options(world: World): world.multiworld.BadgeSellerMinItems[world.player].value, world.multiworld.BadgeSellerMaxItems[world.player].value) + world.multiworld.NyakuzaThugMinShopItems[world.player].value = min( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.NyakuzaThugMaxShopItems[world.player].value = max( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.DWShuffleCountMin[world.player].value = min( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + world.multiworld.DWShuffleCountMax[world.player].value = max( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + total_tps: int = get_total_time_pieces(world) if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 59186d68ff..9245efd937 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -826,11 +826,9 @@ def get_shuffled_region(self, region: str) -> str: def create_thug_shops(world: World): - min_items: int = min(world.multiworld.NyakuzaThugMinShopItems[world.player].value, - world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value - max_items: int = max(world.multiworld.NyakuzaThugMaxShopItems[world.player].value, - world.multiworld.NyakuzaThugMinShopItems[world.player].value) + max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value count: int = -1 step: int = 0 old_name: str = "" @@ -877,6 +875,7 @@ def create_events(world: World) -> int: if not is_location_valid(world, name): continue + item_name: str = name if world.is_dw(): if name in snatcher_coins.keys(): name = f"{name} ({data.region})" @@ -887,15 +886,15 @@ def create_events(world: World) -> int: if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: continue - event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) + event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world) event.show_in_spoiler = False count += 1 return count -def create_event(name: str, region: Region, world: World) -> Location: +def create_event(name: str, item_name: str, region: Region, world: World) -> Location: event = HatInTimeLocation(world.player, name, None, region) region.locations.append(event) - event.place_locked_item(HatInTimeItem(name, ItemClassification.progression, None, world.player)) + event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) return event From 311966ca3a23f9f119e8f2be20a4bee4e03e119b Mon Sep 17 00:00:00 2001 From: CookieCat Date: Fri, 27 Oct 2023 20:46:42 -0400 Subject: [PATCH 2/8] some slight touch ups --- worlds/ahit/DeathWishRules.py | 6 +++--- worlds/ahit/Locations.py | 10 +++++----- worlds/ahit/Regions.py | 4 ++-- worlds/ahit/__init__.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 5a97518499..c448484036 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -321,7 +321,7 @@ def set_candle_dw_rules(name: str, world: World): elif name == "Camera Tourist": add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) - and state.has("Triple Enemy Picture", world.player)) + and state.has("Triple Enemy Photo", world.player)) elif "Snatcher Coins" in name: for coin in required_snatcher_coins[name]: @@ -408,8 +408,8 @@ def create_enemy_events(world: World): continue region = world.multiworld.get_region(name, world.player) - event = HatInTimeLocation(world.player, f"Triple Enemy Picture - {name}", None, region) - event.place_locked_item(HatInTimeItem("Triple Enemy Picture", ItemClassification.progression, None, world.player)) + event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player)) region.locations.append(event) event.show_in_spoiler = False if name == "The Mustache Gauntlet": diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 00ec578624..bf31c8cba8 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -4,6 +4,9 @@ from typing import Dict from .Options import TasksanityCheckCount +TASKSANITY_START_ID = 2000300204 + + def get_total_locations(world: World) -> int: total: int = 0 @@ -96,7 +99,7 @@ def is_location_valid(world: World, location: str) -> bool: def get_location_names() -> Dict[str, int]: names = {name: data.id for name, data in location_table.items()} - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(TasksanityCheckCount.range_end): names.setdefault(f"Tasksanity Check {i+1}", id_start+i) @@ -107,10 +110,6 @@ def get_location_names() -> Dict[str, int]: return names -def get_tasksanity_start_id() -> int: - return 2000300204 - - ahit_locations = { "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), @@ -848,6 +847,7 @@ zero_jumps = { dlc_flags=HatDLC.dlc2_dw), } +# noinspection PyDictDuplicateKeys snatcher_coins = { "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 9245efd937..807f1ee77f 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -2,7 +2,7 @@ from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ - shop_locations, get_tasksanity_start_id, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard + shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard import typing from .Rules import set_rift_rules, get_difficulty @@ -439,7 +439,7 @@ def create_rift_connections(world: World, region: Region): def create_tasksanity_locations(world: World): ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(world.multiworld.TasksanityCheckCount[world.player].value): location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 64b3febc3e..0ed14c6376 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,7 +1,7 @@ from BaseClasses import Item, ItemClassification, Tutorial from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region -from .Locations import location_table, contract_locations, is_location_valid, get_location_names, get_tasksanity_start_id +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID from .Rules import set_rules from .Options import ahit_options, slot_data_options, adjust_options from .Types import HatType, ChapterIndex, HatInTimeItem @@ -252,7 +252,7 @@ class HatInTimeWorld(World): if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: ship_shape_region = get_shuffled_region(self, "Ship Shape") - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(self.multiworld.TasksanityCheckCount[self.player].value): new_hint_data[id_start+i] = ship_shape_region From a262b13c5fb1b5fe53f1ae6f567a604d22eeb83a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 4 Nov 2023 17:14:03 -0400 Subject: [PATCH 3/8] rewind --- 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 - test/general/test_memory.py | 16 + test/utils/test_caches.py | 66 ++++ worlds/AutoWorld.py | 36 +- worlds/__init__.py | 42 ++- worlds/_bizhawk/context.py | 63 +++- worlds/adventure/Rom.py | 6 +- worlds/ahit/Regions.py | 9 +- 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 +- 104 files changed, 1545 insertions(+), 1190 deletions(-) create mode 100644 test/general/test_memory.py create mode 100644 test/utils/test_caches.py 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/test/general/test_memory.py b/test/general/test_memory.py new file mode 100644 index 0000000000..e352b9e875 --- /dev/null +++ b/test/general/test_memory.py @@ -0,0 +1,16 @@ +import unittest + +from worlds.AutoWorld import AutoWorldRegister +from . import setup_solo_multiworld + + +class TestWorldMemory(unittest.TestCase): + def test_leak(self): + """Tests that worlds don't leak references to MultiWorld or themselves with default options.""" + import gc + import weakref + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest("Game", game_name=game_name): + weak = weakref.ref(setup_solo_multiworld(world_type)) + gc.collect() + self.assertFalse(weak(), "World leaked a reference") diff --git a/test/utils/test_caches.py b/test/utils/test_caches.py new file mode 100644 index 0000000000..fc681611f0 --- /dev/null +++ b/test/utils/test_caches.py @@ -0,0 +1,66 @@ +# Tests for caches in Utils.py + +import unittest +from typing import Any + +from Utils import cache_argsless, cache_self1 + + +class TestCacheArgless(unittest.TestCase): + def test_cache(self) -> None: + @cache_argsless + def func_argless() -> object: + return object() + + self.assertTrue(func_argless() is func_argless()) + + if __debug__: # assert only available with __debug__ + def test_invalid_decorator(self) -> None: + with self.assertRaises(Exception): + @cache_argsless # type: ignore[arg-type] + def func_with_arg(_: Any) -> None: + pass + + +class TestCacheSelf1(unittest.TestCase): + def test_cache(self) -> None: + class Cls: + @cache_self1 + def func(self, _: Any) -> object: + return object() + + o1 = Cls() + o2 = Cls() + self.assertTrue(o1.func(1) is o1.func(1)) + self.assertFalse(o1.func(1) is o1.func(2)) + self.assertFalse(o1.func(1) is o2.func(1)) + + def test_gc(self) -> None: + # verify that we don't keep a global reference + import gc + import weakref + + class Cls: + @cache_self1 + def func(self, _: Any) -> object: + return object() + + o = Cls() + _ = o.func(o) # keep a hard ref to the result + r = weakref.ref(o) # keep weak ref to the cache + del o # remove hard ref to the cache + gc.collect() + self.assertFalse(r()) # weak ref should be dead now + + if __debug__: # assert only available with __debug__ + def test_no_self(self) -> None: + with self.assertRaises(Exception): + @cache_self1 # type: ignore[arg-type] + def func() -> Any: + pass + + def test_too_many_args(self) -> None: + with self.assertRaises(Exception): + @cache_self1 # type: ignore[arg-type] + def func(_1: Any, _2: Any, _3: Any) -> Any: + pass 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/ahit/Regions.py b/worlds/ahit/Regions.py index 807f1ee77f..2cabef46ab 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -309,10 +309,10 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -431,9 +431,10 @@ def create_rift_connections(world: World, region: Region): connect_regions(act_region, region, entrance_name, world.player) i += 1 - # fix for some weird keyerror from tests + # fix for some weird keyerror if region.name == "Time Rift - Rumbi Factory": for entrance in region.entrances: + print(entrance.name) world.multiworld.get_entrance(entrance.name, world.player) @@ -631,8 +632,8 @@ def randomize_act_entrances(world: World): candidate = c break + # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) - # print(region, candidate) # Vanilla if candidate.name == region.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. From bd8698e1fd2b8433e3ce1096b7e6cb352c6022aa Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sat, 4 Nov 2023 17:55:48 -0400 Subject: [PATCH 4/8] a --- 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/ahit/Regions.py | 9 +- 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 45893 -> 45570 bytes worlds/pokemon_rb/basepatch_red.bsdiff4 | Bin 45875 -> 45511 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 +- 102 files changed, 1190 insertions(+), 1463 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a70dd70a92..d35739c324 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,15 +1,14 @@ 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 Counter, deque -from collections.abc import Collection, MutableSequence +from collections import ChainMap, Counter, deque +from collections.abc import Collection from enum import IntEnum, IntFlag from typing import Any, Callable, Dict, Iterable, Iterator, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union, \ Type, ClassVar @@ -48,6 +47,7 @@ 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: RegionManager + regions: List[Region] itempool: List[Item] is_race: bool = False precollected_items: Dict[int, List[Item]] @@ -92,34 +92,6 @@ 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()) @@ -128,12 +100,16 @@ class MultiWorld(): self.glitch_triforce = False self.algorithm = 'balanced' self.groups = {} - self.regions = self.RegionManager(players) + self.regions = [] 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 @@ -161,6 +137,7 @@ 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') @@ -222,6 +199,7 @@ 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]) @@ -325,15 +303,11 @@ class MultiWorld(): def player_ids(self) -> Tuple[int, ...]: return tuple(range(1, self.players + 1)) - @Utils.cache_self1 + @functools.lru_cache() 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) - @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 + @functools.lru_cache() 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) @@ -355,17 +329,41 @@ 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.regions.region_cache[player].values() + return self.regions if player is None else self._region_cache[player].values() - def get_region(self, region_name: str, player: int) -> Region: - return self.regions.region_cache[player][region_name] + 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_entrance(self, entrance_name: str, player: int) -> Entrance: - return self.regions.entrance_cache[player][entrance_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_location(self, location_name: str, player: int) -> Location: - return self.regions.location_cache[player][location_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_all_state(self, use_cache: bool) -> CollectionState: cached = getattr(self, "_all_state", None) @@ -426,22 +424,28 @@ class MultiWorld(): logging.debug('Placed %s at %s', item, location) - 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 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 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) -> Iterable[Location]: + 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] if player is not 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)) + 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 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] @@ -463,17 +467,16 @@ 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 = relevant_cache.get(location_name, None) - if location and location.item is None: + location = self._location_cache.get((location_name, player), None) + if location is not None 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(item.player): + for location in self.get_unfilled_locations(): if temp_state.can_reach(location) and not self.state.can_reach(location): return True @@ -605,7 +608,7 @@ PathValue = Tuple[str, Optional["PathValue"]] class CollectionState(): - prog_items: Dict[int, Counter[str]] + prog_items: typing.Counter[Tuple[str, int]] multiworld: MultiWorld reachable_regions: Dict[int, Set[Region]] blocked_connections: Dict[int, Set[Entrance]] @@ -617,7 +620,7 @@ class CollectionState(): additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = [] def __init__(self, parent: MultiWorld): - self.prog_items = {player: Counter() for player in parent.player_ids} + self.prog_items = Counter() 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()} @@ -665,7 +668,7 @@ class CollectionState(): def copy(self) -> CollectionState: ret = CollectionState(self.multiworld) - ret.prog_items = copy.deepcopy(self.prog_items) + ret.prog_items = self.prog_items.copy() 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 @@ -709,23 +712,23 @@ class CollectionState(): self.collect(event.item, True, event) def has(self, item: str, player: int, count: int = 1) -> bool: - return self.prog_items[player][item] >= count + return self.prog_items[item, player] >= 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[player][item] for item in items) + return all(self.prog_items[item, player] 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[player][item] for item in items) + return any(self.prog_items[item, player] for item in items) def count(self, item: str, player: int) -> int: - return self.prog_items[player][item] + return self.prog_items[item, player] 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[player][item_name] + found += self.prog_items[item_name, player] if found >= count: return True return False @@ -733,11 +736,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[player][item_name] + found += self.prog_items[item_name, player] return found def item_count(self, item: str, player: int) -> int: - return self.prog_items[player][item] + return self.prog_items[item, player] def collect(self, item: Item, event: bool = False, location: Optional[Location] = None) -> bool: if location: @@ -746,7 +749,7 @@ class CollectionState(): changed = self.multiworld.worlds[item.player].collect(self, item) if not changed and event: - self.prog_items[item.player][item.name] += 1 + self.prog_items[item.name, item.player] += 1 changed = True self.stale[item.player] = True @@ -813,83 +816,15 @@ 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.EntranceRegister(multiworld.regions) - self._locations = self.LocationRegister(multiworld.regions) + self.exits = [] + self.locations = [] 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) @@ -920,7 +855,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) -> entrance_type: + rule: Optional[Callable[[CollectionState], bool]] = None) -> None: """ Connects this Region to another Region, placing the provided rule on the connection. @@ -931,7 +866,6 @@ 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 c9660ab708..9d5dc0b457 100644 --- a/Fill.py +++ b/Fill.py @@ -15,10 +15,6 @@ 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: @@ -30,7 +26,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, name: str = "Unknown") -> None: + allow_partial: bool = False, allow_excluded: bool = False) -> None: """ :param world: Multiworld to be filled. :param base_state: State assumed before fill. @@ -42,20 +38,16 @@ 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() @@ -160,15 +152,9 @@ 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, []) @@ -212,8 +198,6 @@ 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 @@ -263,12 +247,6 @@ 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 @@ -304,7 +282,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, name="Accessibility Corrections") + fill_restrictive(world, state, locations, pool) def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations): @@ -374,25 +352,23 @@ 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, name=f"Local Early Items P{player}") + player_local, lock=True, allow_partial=True) 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, - name="Early Items") + fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True) 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, name=f"Local Early Progression P{player}") + player_local, lock=True, allow_partial=True) 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, - name="Early Progression") + fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True) 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 " @@ -446,14 +422,13 @@ 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, - name="Priority") + fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking) accessibility_corrections(world, world.state, prioritylocations, progitempool) defaultlocations = prioritylocations + defaultlocations if progitempool: - # "advancement/progression fill" - fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression") + # "progression fill" + fill_restrictive(world, world.state, defaultlocations, progitempool) 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 8113d8a0d7..34a0084e8d 100644 --- a/Generate.py +++ b/Generate.py @@ -7,8 +7,8 @@ import random import string import urllib.parse import urllib.request -from collections import Counter -from typing import Any, Dict, Tuple, Union +from collections import ChainMap, Counter +from typing import Any, Callable, 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) - return callback(erargs, seed) + callback(erargs, seed) def read_weights_yamls(path) -> Tuple[Any, ...]: @@ -639,15 +639,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): if __name__ == '__main__': import atexit confirmation = atexit.register(input, "Press enter to close.") - 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." + main() # in case of error-free exit should not need confirmation atexit.unregister(confirmation) diff --git a/Main.py b/Main.py index 691b88b137..0995d2091f 100644 --- a/Main.py +++ b/Main.py @@ -122,6 +122,10 @@ 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: @@ -229,7 +233,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: @@ -263,6 +267,7 @@ 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 062d7a7cbe..0909c61382 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, death_text: str = "") -> None: + async def handle_deathlink_state(self, currently_dead: bool) -> 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(death_text) + await self.send_death() # 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 bb68602cce..5fb037a173 100644 --- a/Utils.py +++ b/Utils.py @@ -5,7 +5,6 @@ import json import typing import builtins import os -import itertools import subprocess import sys import pickle @@ -74,8 +73,6 @@ 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]: @@ -93,31 +90,6 @@ 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)) @@ -174,16 +146,12 @@ 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", "data/lua"): + for dn in ("Players", "data/sprites"): shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True) - 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) + for fn in ("manifest.json",): + shutil.copy2(local_path(fn), user_path(fn)) return os.path.join(user_path.cached_path, *path) @@ -289,13 +257,15 @@ def get_public_ipv6() -> str: return ip -OptionsType = Settings # TODO: remove when removing get_options +OptionsType = Settings # TODO: remove ~2 versions after 0.4.1 -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() +@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 persistent_store(category: str, key: typing.Any, value: typing.Any): @@ -935,17 +905,3 @@ 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 1a2aab6d88..785785cde0 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -139,13 +139,7 @@ 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 fb1ccd2d6f..74f423df1f 100644 --- a/WebHostLib/static/assets/faq/faq_en.md +++ b/WebHostLib/static/assets/faq/faq_en.md @@ -2,62 +2,13 @@ ## What is a randomizer? -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 +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 game, you might first find item C, then A, then B. -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). +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. ## What happens if an item is placed somewhere it is impossible to get? @@ -66,15 +17,53 @@ 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: -[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago). +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). -There, you will find examples of games in the `worlds` folder: -[/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds). +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). -You may also find developer documentation in the `docs` folder: -[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). +You may also find developer documentation in the docs folder +at [/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 3811bd42ba..bdd121eff5 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-options.json`)).then((response) => { + fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.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.#buildItemPoolDiv(); + const itemPoolDiv = this.#buildItemsDiv(); gameDiv.appendChild(itemPoolDiv); const hintsDiv = this.#buildHintsDiv(); gameDiv.appendChild(hintsDiv); - const locationsDiv = this.#buildPriorityExclusionDiv(); + const locationsDiv = this.#buildLocationsDiv(); gameDiv.appendChild(locationsDiv); collapseButton.addEventListener('click', () => { @@ -734,17 +734,107 @@ class GameSettings { break; case 'items-list': - const itemsList = this.#buildItemsDiv(settingName); + 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)); + }); + settingWrapper.appendChild(itemsList); break; case 'locations-list': - const locationsList = this.#buildLocationsDiv(settingName); + 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)); + }); + settingWrapper.appendChild(locationsList); break; case 'custom-list': - const customList = this.#buildListDiv(settingName, this.data.gameSettings[settingName].options); + 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)); + }); + settingWrapper.appendChild(customList); break; @@ -759,7 +849,7 @@ class GameSettings { return settingsWrapper; } - #buildItemPoolDiv() { + #buildItemsDiv() { const itemsDiv = document.createElement('div'); itemsDiv.classList.add('items-div'); @@ -968,7 +1058,35 @@ class GameSettings { itemHintsWrapper.classList.add('hints-wrapper'); itemHintsWrapper.innerText = 'Starting Item Hints'; - const itemHintsDiv = this.#buildItemsDiv('start_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); + }); + itemHintsWrapper.appendChild(itemHintsDiv); itemHintsContainer.appendChild(itemHintsWrapper); @@ -977,7 +1095,35 @@ class GameSettings { locationHintsWrapper.classList.add('hints-wrapper'); locationHintsWrapper.innerText = 'Starting Location Hints'; - const locationHintsDiv = this.#buildLocationsDiv('start_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); + }); + locationHintsWrapper.appendChild(locationHintsDiv); itemHintsContainer.appendChild(locationHintsWrapper); @@ -985,7 +1131,7 @@ class GameSettings { return hintsDiv; } - #buildPriorityExclusionDiv() { + #buildLocationsDiv() { const locationsDiv = document.createElement('div'); locationsDiv.classList.add('locations-div'); const locationsHeader = document.createElement('h3'); @@ -1005,7 +1151,35 @@ class GameSettings { priorityLocationsWrapper.classList.add('locations-wrapper'); priorityLocationsWrapper.innerText = 'Priority Locations'; - const priorityLocationsDiv = this.#buildLocationsDiv('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); + }); + priorityLocationsWrapper.appendChild(priorityLocationsDiv); locationsContainer.appendChild(priorityLocationsWrapper); @@ -1014,7 +1188,35 @@ class GameSettings { excludeLocationsWrapper.classList.add('locations-wrapper'); excludeLocationsWrapper.innerText = 'Exclude Locations'; - const excludeLocationsDiv = this.#buildLocationsDiv('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); + }); + excludeLocationsWrapper.appendChild(excludeLocationsDiv); locationsContainer.appendChild(excludeLocationsWrapper); @@ -1022,71 +1224,6 @@ 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 8a66ca2370..cc5231634e 100644 --- a/WebHostLib/static/styles/weighted-options.css +++ b/WebHostLib/static/styles/weighted-options.css @@ -292,12 +292,6 @@ 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 8eb471be39..2b943a22b0 100644 --- a/WebHostLib/templates/lttpMultiTracker.html +++ b/WebHostLib/templates/lttpMultiTracker.html @@ -153,7 +153,7 @@ {%- endif -%} {% endif %} {%- endfor -%} - {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} + {{ 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 1a3d353de1..40d89eb4c6 100644 --- a/WebHostLib/templates/multiTracker.html +++ b/WebHostLib/templates/multiTracker.html @@ -55,7 +55,7 @@ {{ checks["Total"] }}/{{ locations[player] | length }} - {{ "{0:.2f}".format(percent_total_checks_done[team][player]) }} + {{ percent_total_checks_done[team][player] }} {%- if activity_timers[team, player] -%} {{ activity_timers[team, player].total_seconds() }} {%- else -%} @@ -72,13 +72,7 @@ All Games {{ completed_worlds }}/{{ players|length }} Complete {{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }} - - {% if total_locations[team] == 0 %} - 100 - {% else %} - {{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }} - {% endif %} - + {{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 55b98df59e..0d9ead7951 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1532,11 +1532,9 @@ 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] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 - ) + percent_total_checks_done[team][player] = int(checks_done[team][player]["Total"] / + len(player_locations) * 100) \ + if player_locations else 100 activity_timers = {} now = datetime.datetime.utcnow() @@ -1692,13 +1690,10 @@ 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"] = len(locations_checked) - - percent_total_checks_done[team][player] = ( - checks_done[team][player]["Total"] / len(player_locations) * 100 - if player_locations - else 100 - ) + 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 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 2054c2d187..5fe4df2014 100644 --- a/test/bases.py +++ b/test/bases.py @@ -1,4 +1,3 @@ -import sys import typing import unittest from argparse import Namespace @@ -108,36 +107,11 @@ 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) @@ -310,7 +284,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 = list(self.multiworld.get_locations(1)) + locations = self.multiworld.get_locations(1).copy() 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 1e469ef04d..4e8cc2edb7 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.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") + 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") 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 79285d3a63..9408f95b16 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 Settings(None).items(): + for option_key, option_set in Utils.get_default_options().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 = Settings(None) + utils_options = Utils.get_default_options() 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 63b3b0f364..2e609a756f 100644 --- a/test/general/test_locations.py +++ b/test/general/test_locations.py @@ -36,6 +36,7 @@ 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()) @@ -45,12 +46,14 @@ 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 d05797cf9e..d4fe0f49a2 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -4,7 +4,6 @@ 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 @@ -18,8 +17,6 @@ if TYPE_CHECKING: from . import GamesPackage from settings import Group -perf_logger = logging.getLogger("performance") - class AutoWorldRegister(type): world_types: Dict[str, Type[World]] = {} @@ -106,24 +103,10 @@ 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 = _timed_call(method, *args, multiworld=multiworld, player=player) + ret = method(*args) except Exception as e: message = f"Exception in {method} for player {player}, named {multiworld.player_name[player]}." if sys.version_info >= (3, 11, 0): @@ -149,15 +132,18 @@ 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.") - call_stage(multiworld, method_name, *args) + 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) 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 sorted(world_types, key=lambda world: world.__name__): + for world_type in world_types: stage_callable = getattr(world_type, f"stage_{method_name}", None) if stage_callable: - _timed_call(stage_callable, multiworld, *args) + stage_callable(multiworld, *args) class WebWorld: @@ -414,16 +400,16 @@ class World(metaclass=AutoWorldRegister): def collect(self, state: "CollectionState", item: "Item") -> bool: name = self.collect_item(state, item) if name: - state.prog_items[self.player][name] += 1 + state.prog_items[name, self.player] += 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[self.player][name] -= 1 - if state.prog_items[self.player][name] < 1: - del (state.prog_items[self.player][name]) + state.prog_items[name, self.player] -= 1 + if state.prog_items[name, self.player] < 1: + del (state.prog_items[name, self.player]) return True return False diff --git a/worlds/__init__.py b/worlds/__init__.py index 40e0b20f19..c6208fa9a1 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -5,20 +5,19 @@ import typing import warnings import zipimport -from Utils import user_path, local_path +folder = os.path.dirname(__file__) -local_folder = os.path.dirname(__file__) -user_folder = user_path("worlds") if user_path() != local_path() else None - -__all__ = ( +__all__ = { "lookup_any_item_id_to_name", "lookup_any_location_id_to_name", "network_data_package", "AutoWorldRegister", "world_sources", - "local_folder", - "user_folder", -) + "folder", +} + +if typing.TYPE_CHECKING: + from .AutoWorld import World class GamesData(typing.TypedDict): @@ -42,13 +41,13 @@ class WorldSource(typing.NamedTuple): is_zip: bool = False relative: bool = True # relative to regular world import folder - def __repr__(self) -> str: + def __repr__(self): 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(local_folder, self.path) + return os.path.join(folder, self.path) return self.path def load(self) -> bool: @@ -57,7 +56,6 @@ 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]) @@ -74,7 +72,7 @@ class WorldSource(typing.NamedTuple): importlib.import_module(f".{self.path}", "worlds") return True - except Exception: + except Exception as e: # A single world failing can still mean enough is working for the user, log and carry on import traceback import io @@ -89,16 +87,14 @@ class WorldSource(typing.NamedTuple): # find potential world containers, currently folders and zip-importable .apworld's world_sources: typing.List[WorldSource] = [] -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)) +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)) # import all submodules to trigger AutoWorldRegister world_sources.sort() @@ -109,7 +105,7 @@ lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {} games: typing.Dict[str, GamesPackage] = {} -from .AutoWorld import AutoWorldRegister # noqa: E402 +from .AutoWorld import AutoWorldRegister # 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 ccf747f15a..5d865f3321 100644 --- a/worlds/_bizhawk/context.py +++ b/worlds/_bizhawk/context.py @@ -5,7 +5,6 @@ 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 @@ -22,13 +21,6 @@ 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""" @@ -43,8 +35,6 @@ 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 @@ -55,8 +45,6 @@ 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 @@ -73,41 +61,10 @@ 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 @@ -152,13 +109,12 @@ 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 and not ctx.server.socket.closed: + if ctx.server is not None: 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: @@ -180,14 +136,15 @@ async def _game_watcher(ctx: BizHawkClientContext): except NotConnectedError: continue - # 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 + # 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() - # 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 9f1ca3fe5e..62c4019718 100644 --- a/worlds/adventure/Rom.py +++ b/worlds/adventure/Rom.py @@ -6,8 +6,9 @@ from typing import Optional, Any import Utils from .Locations import AdventureLocation, LocationData -from settings import get_settings +from Utils import OptionsType from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer +from itertools import chain import bsdiff4 @@ -312,8 +313,9 @@ 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 = get_settings()["adventure_options"]["rom_file"] + file_name = options["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/ahit/Regions.py b/worlds/ahit/Regions.py index 2cabef46ab..807f1ee77f 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -309,10 +309,10 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -431,10 +431,9 @@ def create_rift_connections(world: World, region: Region): connect_regions(act_region, region, entrance_name, world.player) i += 1 - # fix for some weird keyerror + # fix for some weird keyerror from tests if region.name == "Time Rift - Rumbi Factory": for entrance in region.entrances: - print(entrance.name) world.multiworld.get_entrance(entrance.name, world.player) @@ -632,8 +631,8 @@ def randomize_act_entrances(world: World): candidate = c break - # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) + # print(region, candidate) # Vanilla if candidate.name == region.name: diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index edc68473b9..22ef2a39a8 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -520,8 +520,7 @@ 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, - ctx.player_names[ctx.slot] + " ran out of hearts." if ctx.slot else "") + await ctx.handle_deathlink_state(currently_dead) 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 a68acf7288..630d61e019 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,8 +264,7 @@ 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, - name="LttP Dungeon Items") + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True) dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 88a2d899fc..806a420f41 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -293,6 +293,7 @@ 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 e1ae0cc6e6..47cea8c20e 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(player): - if location.address is None or location.shop_slot is not None: + for location in world.get_locations(): + if location.player != player or 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 = list(world.get_entrances(player)) + all_entrances = [entrance for entrance in world.get_entrances() if entrance.player == 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 469f4f82ee..1fddecd8f4 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -197,13 +197,8 @@ 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 - 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_entrance('Old Man S&Q', player), lambda state: state.can_reach('Old Man', 'Location', player)) 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)) @@ -1531,16 +1526,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 and region.is_light_world + return region.is_light_world def is_link(region): - return region and region.is_dark_world + return region.is_dark_world else: def is_bunny(region): - return region and region.is_dark_world + return region.is_dark_world def is_link(region): - return region and region.is_light_world + return 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 @@ -1608,20 +1603,21 @@ 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 region_exit in region.exits: - add_rule(region_exit, rule) + for exit in region.exits: + add_rule(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(player): - if is_bunny(entrance.connected_region): + for entrance in world.get_entrances(): + if entrance.player == player and 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 c0f2e2236e..f17eb1eadb 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -348,6 +348,7 @@ 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): @@ -618,4 +619,6 @@ 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 a6aefc7412..4b6bc54111 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[player]['Moon Pearl'] += 1 + fake_state.prog_items['Moon Pearl', player] += 1 return fake_state diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d89e65c59d..65e36da3bd 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -470,8 +470,7 @@ 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, - name="LttP Dungeon Prizes") + fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True) except FillError as e: lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e, attempts - attempt) @@ -586,26 +585,27 @@ class ALTTPWorld(World): for player in checks_in_area: checks_in_area[player]["Total"] = 0 - 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 + + 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 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[player][item] >= count + return self.prog_items[item, player] >= count diff --git a/worlds/alttp/test/dungeons/TestDungeon.py b/worlds/alttp/test/dungeons/TestDungeon.py index 8ca2791dcf..94c30c3493 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 get_dungeon_item_pool +from worlds.alttp.Dungeons import create_dungeons, 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 3bf4bad475..cdd48e7604 100644 --- a/worlds/archipidle/Rules.py +++ b/worlds/archipidle/Rules.py @@ -5,7 +5,12 @@ from ..generic.Rules import set_rule class ArchipIDLELogic(LogicMixin): def _archipidle_location_is_accessible(self, player_id, items_required): - return sum(self.prog_items[player_id].values()) >= items_required + items_received = 0 + for item in self.prog_items: + if item[1] == player_id: + items_received += 1 + + return items_received >= items_required def set_rules(world: MultiWorld, player: int): diff --git a/worlds/blasphemous/Options.py b/worlds/blasphemous/Options.py index 127a1dc776..ea304d22ed 100644 --- a/worlds/blasphemous/Options.py +++ b/worlds/blasphemous/Options.py @@ -67,7 +67,6 @@ 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 5d88292131..4218fa94cf 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -578,12 +578,11 @@ 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, 2) + can_cross_gap(state, logic, player, 1) 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), @@ -703,11 +702,10 @@ def rules(blasphemousworld): # Items set_rule(world.get_location("WotBC: Cliffside Child of Moonlight", player), lambda state: ( - can_cross_gap(state, logic, player, 2) + can_cross_gap(state, logic, player, 1) 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", "Ranged Skill"}, player) + or state.has_any({"Lorquiana", "Cante Jondo of the Three Sisters", "Cantina of the Blue Rose", "Cloistered Ruby"}, player) or precise_skips_allowed(logic) )) # Doors diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md index 1ff7f5a903..15223213ac 100644 --- a/worlds/blasphemous/docs/en_Blasphemous.md +++ b/worlds/blasphemous/docs/en_Blasphemous.md @@ -19,7 +19,6 @@ 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 4978500da0..feff148651 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 96fb0529df..bd82660b09 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -14,18 +14,11 @@ 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. - -## Unique Local Commands - -The following command is only available when using the ChecksFinderClient to play with Archipelago. - -- `/resync` Manually trigger a resync. +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 diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index 5792d9c3ab..a11e5c504e 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[player][" coins"] >= coin + return lambda state: state.prog_items[" coins", player] >= coin def has_enough_coin_freemium(player: int, coin: int): - return lambda state: state.prog_items[player][" coins freemium"] >= coin + return lambda state: state.prog_items[" coins freemium", player] >= coin def set_rules(world, player, World_Options: Options.DLCQuestOptions): diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index e4e0a29274..54d27f7b65 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[self.player][suffix] += item.coins + state.prog_items[suffix, self.player] += 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[self.player][suffix] -= item.coins + state.prog_items[suffix, self.player] -= item.coins return change diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 59fa85d916..8962919743 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -26,7 +26,6 @@ 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 commands are only available when using the FF1Client for the Final Fantasy Randomizer. +The following command is 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 def5c32981..a9acbf48f3 100644 --- a/worlds/hk/Items.py +++ b/worlds/hk/Items.py @@ -19,43 +19,18 @@ 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 = ({ - "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"], + +item_name_groups.update({ "Dreamers": {"Herrah", "Monomon", "Lurien"}, - "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"}, + "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"}, }) 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 2dc512eca7..4fe4160b4c 100644 --- a/worlds/hk/Rules.py +++ b/worlds/hk/Rules.py @@ -1,4 +1,5 @@ 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 @@ -38,12 +39,14 @@ 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 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) + 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) diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index c16a108cd1..1a9d4b5d61 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[item.player][effect_name] += effect_value + state.prog_items[effect_name, item.player] += effect_value if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}: - 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) + 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) 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[item.player][effect_name] == effect_value: - del state.prog_items[item.player][effect_name] - state.prog_items[item.player][effect_name] -= effect_value + 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 return change diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index c7b127ef2b..6c89db3891 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[self.player]["RUPEES"] + return self.state.prog_items["RUPEES", self.player] return 0 elif item.endswith("_USED"): return 0 else: item = ladxr_item_to_la_item_name[item] - return self.state.prog_items[self.player].get(item, default) + return self.state.prog_items.get((item, self.player), 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 eaaea5be2f..1d6c85dd64 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -231,7 +231,9 @@ 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(self.player): + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue if r.dungeon_index != item.item_data.dungeon_index: continue for loc in r.locations: @@ -267,7 +269,10 @@ 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(self.player): + for r in self.multiworld.get_regions(): + if r.player != self.player: + continue + # Set aside dungeon locations if r.dungeon_index: self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations @@ -513,7 +518,7 @@ class LinksAwakeningWorld(World): change = super().collect(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] += rupees + state.prog_items["RUPEES", item.player] += rupees return change @@ -521,6 +526,6 @@ class LinksAwakeningWorld(World): change = super().remove(state, item) if change: rupees = self.rupees.get(item.name, 0) - state.prog_items[item.player]["RUPEES"] -= rupees + state.prog_items["RUPEES", item.player] -= rupees return change diff --git a/worlds/lufia2ac/Rom.py b/worlds/lufia2ac/Rom.py index 446668d392..1da8d235a6 100644 --- a/worlds/lufia2ac/Rom.py +++ b/worlds/lufia2ac/Rom.py @@ -3,7 +3,7 @@ import os from typing import Optional import Utils -from settings import get_settings +from Utils import OptionsType from worlds.Files import APDeltaPatch L2USHASH: str = "6efc477d6203ed2b3b9133c1cd9e9c5d" @@ -35,8 +35,9 @@ 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 = get_settings()["lufia2ac_options"]["rom_file"] + file_name = options["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 de34570d02..2c66a024ca 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 3fe13a3cb4..0771989ffc 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[self.player]["Shards"] += shard_count + state.prog_items["Shards", self.player] += shard_count return super().collect_item(state, item, remove) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 187f1fdf19..fa992e1e11 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"{self.multiworld.get_out_file_name_base(self.player)}.apmc" + filename = f"AP_{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 7ffa4665fd..854034d5a8 100644 --- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md +++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md @@ -72,10 +72,3 @@ 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 5b3ef40e54..bd07fef7af 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 Nightingale|7-2|Give Up TREATMENT Vol.2|True|4|7|10| +Crimson Nightingle|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,11 +488,4 @@ 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| -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 +Shun-ran|66-9|Miku in Museland|False|4|7|9| \ No newline at end of file diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index bfe321b64a..63ce123c93 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 = 11 + data_version = 10 web = MuseDashWebWorld() # Necessary Data diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py index c859a80394..ca53c96233 100644 --- a/worlds/noita/Items.py +++ b/worlds/noita/Items.py @@ -44,18 +44,20 @@ 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, weights: Dict[str, int], count: int) -> List[str]: - filler_pool = weights.copy() +def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]: + filler_pool = filler_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=count) + return multiworld.random.choices( + population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=random_count + ) def create_all_items(multiworld: MultiWorld, player: int) -> None: - locations_to_fill = len(multiworld.get_unfilled_locations(player)) + sum_locations = len(multiworld.get_unfilled_locations(player)) itempool = ( create_fixed_item_pool() @@ -64,18 +66,9 @@ def create_all_items(multiworld: MultiWorld, player: int) -> None: + create_kantele(multiworld.victory_condition[player]) ) - # 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)) + random_count = sum_locations - len(itempool) + itempool += create_random_items(multiworld, player, random_count) - itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool)) multiworld.itempool += [create_item(player, name) for name in itempool] @@ -91,8 +84,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, 1), - "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful, 1), + "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful), + "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful), "Kantele": ItemData(110012, "Wands", ItemClassification.useful), "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), @@ -102,46 +95,43 @@ 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, 1), + "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful), "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, 1), - "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler, 1), + "Kammi": ItemData(110028, "Items", ItemClassification.filler), + "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler), "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] = { - **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, + "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, } diff --git a/worlds/noita/Regions.py b/worlds/noita/Regions.py index 561d483b48..a239b437d7 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, List +from typing import Dict, Set from BaseClasses import Entrance, MultiWorld, Region from . import Locations @@ -79,46 +79,70 @@ 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, List[str]] = { - "Menu": ["Forest"], - "Forest": ["Mines", "Floating Island", "Desert", "Snowy Wasteland"], - "Frozen Vault": ["The Vault"], - "Overgrown Cavern": ["Sandcave"], +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"}, ### - "Mines": ["Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake"], - "Lava Lake": ["Abyss Orb Room"], + "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"}, ### - "Coal Pits Holy Mountain": ["Coal Pits"], - "Coal Pits": ["Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"], + "Coal Pits Holy Mountain": {"Coal Pits"}, + "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain", "Ancient Laboratory"}, + "Fungal Caverns": {"Coal Pits"}, ### - "Snowy Depths Holy Mountain": ["Snowy Depths"], - "Snowy Depths": ["Hiisi Base Holy Mountain", "Magical Temple", "Below Lava Lake"], + "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"}, ### - "Hiisi Base Holy Mountain": ["Hiisi Base"], - "Hiisi Base": ["Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"], + "Hiisi Base Holy Mountain": {"Hiisi Base"}, + "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"}, + "Secret Shop": {"Hiisi Base"}, ### - "Underground Jungle Holy Mountain": ["Underground Jungle"], - "Underground Jungle": ["Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", "Lukki Lair", "Snow Chasm"], + "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": {}, ### - "Vault Holy Mountain": ["The Vault"], - "The Vault": ["Frozen Vault", "Temple of the Art Holy Mountain"], + "Vault Holy Mountain": {"The Vault"}, + "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"}, ### - "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"], + "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": {}, ### - "Laboratory Holy Mountain": ["The Laboratory"], - "The Laboratory": ["The Work", "Friend Cave", "The Work (Hell)", "Lake"], + "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)": {}, ### } -noita_regions: List[str] = sorted(set(noita_connections.keys()).union(*noita_connections.values())) +noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values()) diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py index 808dd3a200..3eb6be5a7c 100644 --- a/worlds/noita/Rules.py +++ b/worlds/noita/Rules.py @@ -44,10 +44,12 @@ 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())) @@ -153,12 +155,11 @@ def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: def create_all_rules(multiworld: MultiWorld, player: int) -> None: - 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) + 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 6c4b6428f5..e480c957a6 100644 --- a/worlds/oot/Entrance.py +++ b/worlds/oot/Entrance.py @@ -1,4 +1,6 @@ + from BaseClasses import Entrance +from .Regions import TimeOfDay class OOTEntrance(Entrance): game: str = 'Ocarina of Time' @@ -27,16 +29,16 @@ class OOTEntrance(Entrance): self.connected_region = None return previously_connected - def get_new_target(self, pool_type): + def get_new_target(self): root = self.multiworld.get_region('Root Exits', self.player) - target_entrance = OOTEntrance(self.player, self.multiworld, f'Root -> ({self.name}) ({pool_type})', root) + target_entrance = OOTEntrance(self.player, self.multiworld, 'Root -> ' + self.connected_region.name, root) target_entrance.connect(self.connected_region) target_entrance.replaces = self root.exits.append(target_entrance) return target_entrance - def assume_reachable(self, pool_type): + def assume_reachable(self): if self.assumed == None: - self.assumed = self.get_new_target(pool_type) + self.assumed = self.get_new_target() self.disconnect() return self.assumed diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py index bbdc30490c..3c1b2d78c6 100644 --- a/worlds/oot/EntranceShuffle.py +++ b/worlds/oot/EntranceShuffle.py @@ -2,7 +2,6 @@ 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 @@ -26,12 +25,12 @@ def set_all_entrances_data(world, player): return_entrance.data['index'] = 0x7FFF -def assume_entrance_pool(entrance_pool, ootworld, pool_type): +def assume_entrance_pool(entrance_pool, ootworld): assumed_pool = [] for entrance in entrance_pool: - assumed_forward = entrance.assume_reachable(pool_type) + assumed_forward = entrance.assume_reachable() if entrance.reverse != None and not ootworld.decouple_entrances: - assumed_return = entrance.reverse.assume_reachable(pool_type) + assumed_return = entrance.reverse.assume_reachable() 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): @@ -42,15 +41,15 @@ def assume_entrance_pool(entrance_pool, ootworld, pool_type): return assumed_pool -def build_one_way_targets(world, pool, types_to_include, exclude=(), target_region_names=()): +def build_one_way_targets(world, 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(pool) for entrance in valid_one_way_entrances + return [entrance.get_new_target() for entrance in valid_one_way_entrances if entrance.connected_region.name in target_region_names] - return [entrance.get_new_target(pool) for entrance in valid_one_way_entrances] + return [entrance.get_new_target() for entrance in valid_one_way_entrances] # Abbreviations @@ -424,14 +423,14 @@ multi_interior_regions = { } interior_entrance_bias = { - '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, + '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, 'Market Entrance -> Market Guard House': 2, - 'Goron City -> GC Shop': 1, - 'Zoras Domain -> ZD Shop': 1, + 'ToT Entrance -> Temple of Time': 1, } @@ -444,8 +443,7 @@ def shuffle_random_entrances(ootworld): player = ootworld.player # Gather locations to keep reachable for validation - all_state = ootworld.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state = world.get_all_state(use_cache=True) 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 @@ -525,12 +523,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, pool_type, valid_target_types, exclude=['Prelude of Light Warp -> Temple of Time']) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, 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, pool_type, valid_target_types) + one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, 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)))()) @@ -540,11 +538,14 @@ 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, pool_type) + target_entrance_pools[pool_type] = assume_entrance_pool(entrance_pool, ootworld) # Build all_state and none_state all_state = ootworld.get_state_with_complete_itempool() - none_state = CollectionState(ootworld.multiworld) + 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 # Plando entrances if world.plando_connections[player]: @@ -627,7 +628,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 = ootworld.get_state_with_complete_itempool() + new_all_state = world.get_all_state(use_cache=False) if not world.has_beaten_game(new_all_state, player): raise EntranceShuffleError('Cannot beat game') # Validate world @@ -699,7 +700,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=10): +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): restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances) @@ -744,6 +745,7 @@ 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 @@ -753,7 +755,7 @@ def split_entrances_by_requirements(ootworld, entrances_to_split, assumed_entran if entrance.connected_region: original_connected_regions[entrance] = entrance.disconnect() - all_state = ootworld.get_state_with_complete_itempool() + all_state = world.get_all_state(use_cache=False) restrictive_entrances = [] soft_entrances = [] @@ -791,8 +793,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(locations=ootworld.get_locations()) - none_state.sweep_for_events(locations=ootworld.get_locations()) + all_state.sweep_for_events() + none_state.sweep_for_events() 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 0f1d3f4dcb..f83b34183c 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 ['Thieves Hideout', 'Gerudo Training Ground', 'Ganons Castle']: + if dungeon in ['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 529411f6fc..fa198e0ce1 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -1,12 +1,8 @@ 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 @@ -154,16 +150,11 @@ 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 in {'song', 'dungeon'} and not ootworld.songs_as_items: + if ootworld.shuffle_song_items == 'song' 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: 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) + add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) @@ -185,6 +176,11 @@ 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 @@ -227,8 +223,7 @@ 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.get_state_with_complete_itempool() - all_state.sweep_for_events(locations=ootworld.get_locations()) + all_state = ootworld.multiworld.get_all_state(False) 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 e9c889d6f6..6af19683f4 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): - 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} + 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} def copy_mixin(self, ret) -> CollectionState: ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in @@ -170,19 +170,15 @@ 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) @@ -198,10 +194,8 @@ 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 @@ -495,8 +489,6 @@ 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) @@ -534,10 +526,6 @@ 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(): @@ -567,10 +555,8 @@ class OOTWorld(World): self.multiworld.regions.append(new_region) self.regions.append(new_region) - self._regions_cache[new_region.name] = new_region + self.multiworld._recache() - - # 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'}] @@ -599,8 +585,6 @@ 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 = {} @@ -626,8 +610,6 @@ 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', @@ -640,7 +622,7 @@ class OOTWorld(World): 'Twinrova', 'Links Pocket' ) - boss_rewards = sorted(map(self.create_item, self.item_name_groups['rewards'])) + boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward'] 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] @@ -654,46 +636,9 @@ 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, @@ -713,9 +658,7 @@ class OOTWorld(World): location.internal = True return item - - # Create regions, locations, and entrances - def create_regions(self): + def create_regions(self): # create and link regions if self.logic_rules == 'glitchless' or self.logic_rules == 'no_logic': # enables ER + NL world_type = 'World' else: @@ -728,7 +671,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.get_region('Root')) + start.connect(self.multiworld.get_region('Root', self.player)) create_dungeons(self) self.parser.create_delayed_rules() @@ -739,13 +682,16 @@ class OOTWorld(World): # Bind entrances to vanilla for region in self.regions: for exit in region.exits: - exit.connect(self.get_region(exit.vanilla_connected_region)) + exit.connect(self.multiworld.get_region(exit.vanilla_connected_region, self.player)) - - # 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 = [] @@ -768,16 +714,12 @@ 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: @@ -815,7 +757,6 @@ 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 @@ -828,9 +769,8 @@ 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.get_state_with_complete_itempool() + all_state = self.multiworld.get_all_state(False) 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] @@ -841,6 +781,7 @@ 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: @@ -851,63 +792,35 @@ 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'] - 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) - + world_items = [item for item in self.multiworld.itempool if item.player == self.player] for fill_stage in special_fill_types: - stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), self.pre_fill_items)) + stage_items = list(filter(lambda item: oot_is_item_of_type(item, fill_stage), world_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.pre_fill_items.remove(item) + self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, prefill_state(state), locations, stage_items, + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), 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.pre_fill_items.remove(item) + self.multiworld.itempool.remove(item) self.multiworld.random.shuffle(locations) - fill_restrictive(self.multiworld, prefill_state(state), locations, dungeon_items, + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), locations, dungeon_items, single_player_placement=True, lock=True, allow_excluded=True) # Place songs @@ -923,9 +836,9 @@ class OOTWorld(World): else: raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}") - songs = list(filter(lambda item: item.type == 'Song', self.pre_fill_items)) + songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.multiworld.itempool)) for song in songs: - self.pre_fill_items.remove(song) + self.multiworld.itempool.remove(song) important_warps = (self.shuffle_special_interior_entrances or self.shuffle_overworld_entrances or self.warp_songs or self.spawn_positions) @@ -948,7 +861,7 @@ class OOTWorld(World): while tries: try: self.multiworld.random.shuffle(song_locations) - fill_restrictive(self.multiworld, prefill_state(state), song_locations[:], songs[:], + fill_restrictive(self.multiworld, self.multiworld.get_all_state(False), 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: @@ -970,8 +883,10 @@ 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.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_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_locations = list( filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices, self.multiworld.get_unfilled_locations(player=self.player))) @@ -981,14 +896,30 @@ 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) - self.pre_fill_items = [] # all prefill should be done - fill_restrictive(self.multiworld, prefill_state(state), shop_locations, shop_prog, + 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, 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': @@ -1023,6 +954,48 @@ 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': @@ -1059,6 +1032,30 @@ 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 @@ -1138,7 +1135,6 @@ 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 { @@ -1146,7 +1142,6 @@ class OOTWorld(World): 'collectible_flag_offsets': self.collectible_flag_offsets } - def modify_multidata(self, multidata: dict): # Replace connect name @@ -1161,16 +1156,6 @@ 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] = {} @@ -1217,7 +1202,6 @@ 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") @@ -1227,32 +1211,6 @@ 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 @@ -1260,16 +1218,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[self.player][alt_item_name] += count + state.prog_items[alt_item_name, self.player] += 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[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]) + 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]) return True return super().remove(state, item) @@ -1284,29 +1242,24 @@ class OOTWorld(World): return False def get_shufflable_entrances(self, type=None, only_primary=False): - return [entrance for entrance in self.get_entrances() if ((type == None or entrance.type == type) - and (not only_primary or entrance.primary))] + 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))] 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): - return self.multiworld.get_locations(self.player) + for region in self.regions: + for loc in region.locations: + yield loc def get_location(self, location): return self.multiworld.get_location(location, 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_region(self, region): + return self.multiworld.get_region(region, self.player) def get_entrance(self, entrance): return self.multiworld.get_entrance(entrance, self.player) @@ -1341,8 +1294,9 @@ 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.itempool + self.pre_fill_items: - self.multiworld.worlds[item.player].collect(all_state, item) + for item in self.multiworld.itempool: + if item.player == self.player: + 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) @@ -1382,6 +1336,7 @@ 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', @@ -1400,12 +1355,21 @@ 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 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 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)) return locations - diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md index fa8e148957..b4610878b6 100644 --- a/worlds/oot/docs/en_Ocarina of Time.md +++ b/worlds/oot/docs/en_Ocarina of Time.md @@ -31,10 +31,3 @@ 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 b2ee0702c9..11aa737e0f 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -445,9 +445,13 @@ 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 @@ -463,17 +467,13 @@ class PokemonRedBlueWorld(World): locs = {self.multiworld.get_location("Fossil - Choice A", self.player), self.multiworld.get_location("Fossil - Choice B", self.player)} - if not self.multiworld.key_items_only[self.player]: - rule = None + for loc in locs: if self.multiworld.fossil_check_item_types[self.player] == "key_items": - rule = lambda i: i.advancement + add_item_rule(loc, lambda i: i.advancement) elif self.multiworld.fossil_check_item_types[self.player] == "unique_items": - rule = lambda i: i.name in item_groups["Unique"] + add_item_rule(loc, lambda i: i.name in item_groups["Unique"]) elif self.multiworld.fossil_check_item_types[self.player] == "no_key_items": - rule = lambda i: not i.advancement - if rule: - for loc in locs: - add_item_rule(loc, rule) + add_item_rule(loc, lambda i: not i.advancement) 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,6 +559,7 @@ 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 eb4d83360cd854c12ec7f0983dd3944d6bfa7fd1..b7bdda7fbbed37ff0fb9bff23d33460c38cdd1f3 100644 GIT binary patch literal 45570 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

RP#AGhVah-Ib>@Av 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Τ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 literal 45875 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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 086ec347f3..daefd6b2f7 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -80,9 +80,3 @@ 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 096ab8e0a1..4b191d9176 100644 --- a/worlds/pokemon_rb/rom.py +++ b/worlds/pokemon_rb/rom.py @@ -546,8 +546,10 @@ def generate_output(self, output_directory: str): write_quizzes(self, data, random) - for location in self.multiworld.get_locations(self.player): - if location.party_data: + for location in self.multiworld.get_locations(): + if location.player != self.player: + continue + elif 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 97faf7bff2..9c6621523c 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": 0x75c, - "Option_Blind_Trainers": 0x30c7, - "Option_Trainersanity1": 0x3157, - "Option_Split_Card_Key": 0x3e10, - "Option_Fix_Combat_Bugs": 0x3e11, + "Option_Pitch_Black_Rock_Tunnel": 0x758, + "Option_Blind_Trainers": 0x30c3, + "Option_Trainersanity1": 0x3153, + "Option_Split_Card_Key": 0x3e0c, + "Option_Fix_Combat_Bugs": 0x3e0d, "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 21dceb75e8..0855e7a108 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 0ed0a87b17..79739e85ef 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 = 1 + default = 0 class TotalLocations(Range): @@ -48,8 +48,7 @@ class ScavengersPerEnvironment(Range): display_name = "Scavenger per Environment" range_start = 0 range_end = 1 - default = 0 - + default = 1 class ScannersPerEnvironment(Range): """Explore Mode: The number of scanners locations per environment.""" @@ -58,7 +57,6 @@ 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" @@ -66,7 +64,6 @@ 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" @@ -86,7 +83,6 @@ class ItemPickupStep(Range): range_end = 5 default = 1 - class ShrineUseStep(Range): """ Explore Mode: @@ -135,6 +131,7 @@ class DLC_SOTV(Toggle): display_name = "Enable DLC - SOTV" + class GreenScrap(Range): """Weight of Green Scraps in the item pool. @@ -277,8 +274,25 @@ class ItemWeights(Choice): option_void = 9 + + +# define a class for the weights of the generated item pool. @dataclass -class ROR2Options(PerGameCommonOptions): +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): goal: Goal total_locations: TotalLocations chests_per_stage: ChestsPerEnvironment @@ -296,16 +310,4 @@ class ROR2Options(PerGameCommonOptions): shrine_use_step: ShrineUseStep enable_lunar: AllowLunarItems item_weights: ItemWeights - 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 + item_pool_presets: ItemPoolPresetToggle \ No newline at end of file diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 65c04d06cb..7d94177417 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -96,7 +96,8 @@ 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(player): + for location in multiworld.get_locations(): + if location.player != player: continue # ignore all checks that don't belong to this 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 18bda64784..f7c8519a2a 100644 --- a/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md +++ b/worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md @@ -31,24 +31,4 @@ 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. - -## 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 +for more information on how to change this. \ No newline at end of file diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 3e9015eab7..f208e600b9 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -112,12 +112,15 @@ 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 @@ -291,7 +294,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 not in self.multiworld.regions.entrance_cache[self.player]: + if ((src.Name + "->" + dest.Name, self.player) not in self.multiworld._entrance_cache): 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) @@ -365,7 +368,7 @@ class SMWorld(World): locationsDict[first_local_collected_loc.name]), itemLoc.item.player, True) - for itemLoc in spheres if itemLoc.item.player == self.player and (not progression_only or itemLoc.item.advancement) + for itemLoc in SMWorld.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. @@ -373,10 +376,8 @@ 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. - 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) + if (SMWorld.spheres == None): + SMWorld.spheres = [itemLoc for sphere in self.multiworld.get_spheres() for itemLoc in sorted(sphere, key=lambda location: location.name)] self.itemLocs = [ ItemLocation(copy.copy(ItemManager.Items[itemLoc.item.type @@ -389,7 +390,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 spheres if itemLoc.player == self.player) + first_local_collected_loc = next(itemLoc for itemLoc in SMWorld.spheres if itemLoc.player == self.player) playerItemsItemLocs = get_player_ItemLocation(False) playerProgItemsItemLocs = get_player_ItemLocation(True) @@ -562,8 +563,8 @@ class SMWorld(World): multiWorldItems: List[ByteEdit] = [] idx = 0 vanillaItemTypesCount = 21 - for itemLoc in self.multiworld.get_locations(self.player): - if "Boss" not in locationsDict[itemLoc.name].Class: + for itemLoc in self.multiworld.get_locations(): + if itemLoc.player == self.player and "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 8a10f3edea..a603b61c58 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -88,12 +88,6 @@ 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""" @@ -116,5 +110,4 @@ 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 27b5fc8f7e..7c50ba4708 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -124,9 +124,4 @@ 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)) - 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) + world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 3cc87708e7..6a7a3bd272 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -154,7 +154,6 @@ 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 2cc2ac97d9..e2eb2ac80a 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.player][item.name] += 1 + state.prog_items[item.name, item.player] += 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[item.player][item.name] -= 1 - if state.prog_items[item.player][item.name] < 1: - del (state.prog_items[item.player][item.name]) + state.prog_items[name, item.player] -= 1 + if state.prog_items[name, item.player] < 1: + del (state.prog_items[name, item.player]) return True return False diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index d02a8d02ee..9a8f38cdac 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 self.multiworld.get_locations(self.player): + for location in filter(lambda l: l.player == self.player, self.multiworld.get_locations()): 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 30fe96c9d9..81c4989411 100644 --- a/worlds/stardew_valley/mods/mod_data.py +++ b/worlds/stardew_valley/mods/mod_data.py @@ -21,11 +21,3 @@ 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 9c96de00d3..5455a40e7a 100644 --- a/worlds/stardew_valley/stardew_rule.py +++ b/worlds/stardew_valley/stardew_rule.py @@ -88,7 +88,6 @@ 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] @@ -113,7 +112,6 @@ 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) @@ -141,8 +139,6 @@ 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_ @@ -155,14 +151,11 @@ class Or(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - self.rules = frozenset(simplified_rules) - self._simplified = True - return self + return Or(simplified_rules) class And(StardewRule): rules: FrozenSet[StardewRule] - _simplified: bool def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): rules_list: Set[StardewRule] @@ -187,7 +180,6 @@ 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) @@ -215,8 +207,6 @@ 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_ @@ -229,9 +219,7 @@ class And(StardewRule): if len(simplified_rules) == 1: return simplified_rules[0] - self.rules = frozenset(simplified_rules) - self._simplified = True - return self + return And(simplified_rules) class Count(StardewRule): diff --git a/worlds/stardew_valley/test/TestBackpack.py b/worlds/stardew_valley/test/TestBackpack.py index 378c90e40a..f26a7c1f03 100644 --- a/worlds/stardew_valley/test/TestBackpack.py +++ b/worlds/stardew_valley/test/TestBackpack.py @@ -5,41 +5,40 @@ from .. import options class TestBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla} - 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_in_pool(self): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_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) + 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) class TestBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} - 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_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) - 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) + 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) -class TestBackpackEarlyProgressive(TestBackpackProgressive): +class TestBackpackEarlyProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive} - @property - def run_default_tests(self) -> bool: - # EarlyProgressive is default - return False + 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): - super().test_backpack() + 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="is early"): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + def test_progressive_backpack_is_in_early_pool(self): + 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 46c6685ad5..0142ad0079 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,8 +1,5 @@ -import typing - from BaseClasses import ItemClassification, MultiWorld -from . import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_with_mods, \ - allsanity_options_without_mods, minimal_locations_maximal_items +from . import setup_solo_multiworld, SVTestBase 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 @@ -10,11 +7,11 @@ from ..locations import LocationTags from ..mods.mod_data import ModNames -def get_real_locations(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): +def get_real_locations(tester: SVTestBase, multiworld: MultiWorld): return [location for location in multiworld.get_locations(tester.player) if not location.event] -def get_real_location_names(tester: typing.Union[SVTestBase, SVTestCase], multiworld: MultiWorld): +def get_real_location_names(tester: SVTestBase, multiworld: MultiWorld): return [location.name for location in multiworld.get_locations(tester.player) if not location.event] @@ -118,6 +115,21 @@ 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 @@ -193,17 +205,17 @@ class TestLocationGeneration(SVTestBase): self.assertIn(location.name, location_table) -class TestLocationAndItemCount(SVTestCase): +class TestLocationAndItemCount(SVTestBase): def test_minimal_location_maximal_items_still_valid(self): - min_max_options = minimal_locations_maximal_items() + min_max_options = self.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 = allsanity_options_without_mods() + allsanity_options = self.allsanity_options_without_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -216,7 +228,7 @@ class TestLocationAndItemCount(SVTestCase): def test_allsanity_with_mods_has_at_least_locations(self): expected_locations = 1246 - allsanity_options = allsanity_options_with_mods() + allsanity_options = self.allsanity_options_with_mods() multiworld = setup_solo_multiworld(allsanity_options) number_locations = len(get_real_locations(self, multiworld)) self.assertGreaterEqual(number_locations, expected_locations) @@ -233,11 +245,6 @@ 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")) @@ -409,7 +416,6 @@ 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, @@ -522,7 +528,6 @@ 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 38f59c7490..7f48f9347c 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, SVTestCase, allsanity_options_without_mods +from . import setup_solo_multiworld, SVTestBase from .. import ItemData, StardewValleyWorld from ..items import Group, item_table -class TestItems(SVTestCase): +class TestItems(SVTestBase): def test_can_create_item_of_resource_pack(self): item_name = "Resource Pack: 500 Money" @@ -46,7 +46,7 @@ class TestItems(SVTestCase): def test_correct_number_of_stardrops(self): seed = random.randrange(sys.maxsize) - allsanity_options = allsanity_options_without_mods() + allsanity_options = self.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 3f02643b83..33b2428098 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,57 +1,56 @@ -import unittest from .. import True_ from ..logic import Received, Has, False_, And, Or -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_true_in_and(): + rules = { + "Wood": True_(), + "Rock": True_(), + } + summer = Received("Summer", 0, 1) + assert (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_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_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_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_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_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_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_true_in_or(self): - rule = Or(True_(), Received('Summer', 0, 1)) - self.assertEqual(rule.simplify(), True_()) +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_false_in_and(self): - rule = And(False_(), Received('Summer', 0, 1)) - self.assertEqual(rule.simplify(), False_()) + +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_() diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 02b1ebf643..712aa300d5 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,11 +1,10 @@ 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, SVTestCase, allsanity_options_without_mods, allsanity_options_with_mods +from . import setup_solo_multiworld, SVTestBase 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 @@ -18,21 +17,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: unittest.TestCase, multiworld: MultiWorld): +def assert_can_win(tester: SVTestBase, 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: unittest.TestCase, multiworld: MultiWorld): +def basic_checks(tester: SVTestBase, 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: unittest.TestCase, multiworld: MultiWorld): +def check_no_ginger_island(tester: SVTestBase, 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(): @@ -49,9 +48,9 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestCase): +class TestGenerateDynamicOptions(SVTestBase): def test_given_special_range_when_generate_then_basic_checks(self): - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange): continue @@ -63,7 +62,7 @@ class TestGenerateDynamicOptions(SVTestCase): def test_given_choice_when_generate_then_basic_checks(self): seed = int(random() * pow(10, 18) - 1) - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not option.options: continue @@ -74,7 +73,7 @@ class TestGenerateDynamicOptions(SVTestCase): basic_checks(self, multiworld) -class TestGoal(SVTestCase): +class TestGoal(SVTestBase): 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), @@ -91,7 +90,7 @@ class TestGoal(SVTestCase): self.assertEqual(victory.name, location) -class TestSeasonRandomization(SVTestCase): +class TestSeasonRandomization(SVTestBase): 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) @@ -115,7 +114,7 @@ class TestSeasonRandomization(SVTestCase): self.assertEqual(items.count(Season.progressive), 3) -class TestToolProgression(SVTestCase): +class TestToolProgression(SVTestBase): 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) @@ -148,9 +147,9 @@ class TestToolProgression(SVTestCase): self.assertIn("Purchase Iridium Rod", locations) -class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): +class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestBase): def test_given_special_range_when_generate_exclude_ginger_island(self): - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not isinstance(option, SpecialRange) or option_name == ExcludeGingerIsland.internal_name: continue @@ -163,7 +162,7 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): def test_given_choice_when_generate_exclude_ginger_island(self): seed = int(random() * pow(10, 18) - 1) - options = StardewValleyWorld.options_dataclass.type_hints + options = self.world.options_dataclass.type_hints for option_name, option in options.items(): if not option.options or option_name == ExcludeGingerIsland.internal_name: continue @@ -192,9 +191,9 @@ class TestGenerateAllOptionsWithExcludeGingerIsland(SVTestCase): basic_checks(self, multiworld) -class TestTraps(SVTestCase): +class TestTraps(SVTestBase): def test_given_no_traps_when_generate_then_no_trap_in_pool(self): - world_options = allsanity_options_without_mods() + world_options = self.allsanity_options_without_mods() world_options.update({TrapItems.internal_name: TrapItems.option_no_traps}) multi_world = setup_solo_multiworld(world_options) @@ -210,7 +209,7 @@ class TestTraps(SVTestCase): for value in trap_option.options: if value == "no_traps": continue - world_options = allsanity_options_with_mods() + world_options = self.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] @@ -220,7 +219,7 @@ class TestTraps(SVTestCase): self.assertIn(item, multiworld_items) -class TestSpecialOrders(SVTestCase): +class TestSpecialOrders(SVTestBase): 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 7ebbcece5c..2347ca33db 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 SVTestCase, setup_solo_multiworld +from . import SVTestBase, 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(SVTestCase): +class TestEntranceClassifications(SVTestBase): 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 72337812cd..0847d8a63b 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 = {1: Counter()} + self.multiworld.state.prog_items = 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 b0c4ba2c7b..53181154d3 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,10 +1,8 @@ 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 @@ -15,17 +13,11 @@ from ..options import Cropsanity, SkillProgression, SpecialOrderLocations, Frien BundleRandomization, BundlePrice, FestivalLocations, FriendsanityHeartSize, ExcludeGingerIsland, TrapItems, Goal, Mods -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): +class SVTestBase(WorldTestBase): 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) @@ -42,73 +34,66 @@ class SVTestBase(WorldTestBase, SVTestCase): 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 -@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_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 + 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 9bd9fd614c..2cdb0534d4 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: unittest.TestCase, multiworld: MultiWorld): +def assert_victory_exists(tester: SVTestBase, multiworld: MultiWorld): tester.assertIn(StardewItem("Victory", ItemClassification.progression, None, 1), multiworld.get_items()) -def collect_all_then_assert_can_win(tester: unittest.TestCase, multiworld: MultiWorld): +def collect_all_then_assert_can_win(tester: SVTestBase, 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: unittest.TestCase, multiworld: MultiWorld): +def assert_can_win(tester: SVTestBase, multiworld: MultiWorld): assert_victory_exists(tester, multiworld) collect_all_then_assert_can_win(tester, multiworld) -def assert_same_number_items_locations(tester: unittest.TestCase, multiworld: MultiWorld): +def assert_same_number_items_locations(tester: SVTestBase, 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 36a59ae854..b3ec6f1420 100644 --- a/worlds/stardew_valley/test/long/TestModsLong.py +++ b/worlds/stardew_valley/test/long/TestModsLong.py @@ -1,17 +1,23 @@ -import unittest from typing import List, Union from BaseClasses import MultiWorld -from worlds.stardew_valley.mods.mod_data import all_mods +from worlds.stardew_valley.mods.mod_data import ModNames from worlds.stardew_valley.test import setup_solo_multiworld -from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestCase +from worlds.stardew_valley.test.TestOptions import basic_checks, SVTestBase 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: unittest.TestCase, multiworld: MultiWorld): + +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -24,7 +30,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.T tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestCase): +class TestGenerateModsOptions(SVTestBase): 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 3634dc5fd1..23ac6125e6 100644 --- a/worlds/stardew_valley/test/long/TestOptionsLong.py +++ b/worlds/stardew_valley/test/long/TestOptionsLong.py @@ -1,14 +1,13 @@ -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, SVTestCase +from .. import setup_solo_multiworld, SVTestBase -def basic_checks(tester: unittest.TestCase, multiworld: MultiWorld): +def basic_checks(tester: SVTestBase, multiworld: MultiWorld): assert_can_win(tester, multiworld) assert_same_number_items_locations(tester, multiworld) @@ -21,7 +20,7 @@ def get_option_choices(option) -> Dict[str, int]: return {} -class TestGenerateDynamicOptions(SVTestCase): +class TestGenerateDynamicOptions(SVTestBase): 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 e22c6c3564..0145f471d1 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, SVTestCase +from .. import setup_solo_multiworld, SVTestBase 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: SVTestCase, multiworlds: Dict[int, MultiWorld]): +def check_every_multiworld_is_valid(tester: SVTestBase, 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: SVTestCase, multiworld_id: int, multiworld: MultiWorld): +def check_multiworld_is_valid(tester: SVTestBase, 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: SVTestCase, multiworld_id: int, multiworld assert_festivals_give_access_to_deluxe_scarecrow(tester, multiworld) -class TestGenerateManyWorlds(SVTestCase): +class TestGenerateManyWorlds(SVTestBase): 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 bc81f21963..0265f61731 100644 --- a/worlds/stardew_valley/test/mods/TestBiggerBackpack.py +++ b/worlds/stardew_valley/test/mods/TestBiggerBackpack.py @@ -7,40 +7,45 @@ class TestBiggerBackpackVanilla(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, options.Mods.internal_name: ModNames.big_backpack} - 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_in_pool(self): + item_names = {item.name for item in self.multiworld.get_items()} + self.assertNotIn("Progressive Backpack", item_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) + 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) class TestBiggerBackpackProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive, options.Mods.internal_name: ModNames.big_backpack} - 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_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) - 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) + 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) -class TestBiggerBackpackEarlyProgressive(TestBiggerBackpackProgressive): +class TestBiggerBackpackEarlyProgressive(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive, options.Mods.internal_name: ModNames.big_backpack} - def test_backpack(self): - super().test_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) - with self.subTest(check="is early"): - self.assertIn("Progressive Backpack", self.multiworld.early_items[1]) + 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]) diff --git a/worlds/stardew_valley/test/mods/TestMods.py b/worlds/stardew_valley/test/mods/TestMods.py index 9bdabaf73f..02fd30a6b1 100644 --- a/worlds/stardew_valley/test/mods/TestMods.py +++ b/worlds/stardew_valley/test/mods/TestMods.py @@ -4,17 +4,24 @@ import random import sys from BaseClasses import MultiWorld -from ...mods.mod_data import all_mods -from .. import setup_solo_multiworld, SVTestBase, SVTestCase, allsanity_options_without_mods -from ..TestOptions import basic_checks +from ...mods.mod_data import ModNames +from .. import setup_solo_multiworld +from ..TestOptions import basic_checks, SVTestBase 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: unittest.TestCase, multiworld: MultiWorld): + +def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: SVTestBase, multiworld: MultiWorld): if isinstance(chosen_mods, str): chosen_mods = [chosen_mods] for multiworld_item in multiworld.get_items(): @@ -27,7 +34,7 @@ def check_stray_mod_items(chosen_mods: Union[List[str], str], tester: unittest.T tester.assertTrue(location.mod_name is None or location.mod_name in chosen_mods) -class TestGenerateModsOptions(SVTestCase): +class TestGenerateModsOptions(SVTestBase): def test_given_single_mods_when_generate_then_basic_checks(self): for mod in all_mods: @@ -43,8 +50,6 @@ class TestGenerateModsOptions(SVTestCase): 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): @@ -98,7 +103,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase): self.assertIn(progression_item.name, all_created_items) -class TestModEntranceRando(SVTestCase): +class TestModEntranceRando(unittest.TestCase): def test_mod_entrance_randomization(self): @@ -132,12 +137,12 @@ class TestModEntranceRando(SVTestCase): f"Connections are duplicated in randomization. Seed = {seed}") -class TestModTraps(SVTestCase): +class TestModTraps(SVTestBase): def test_given_traps_when_generate_then_all_traps_in_pool(self): for value in TrapItems.options: if value == "no_traps": continue - world_options = allsanity_options_without_mods() + world_options = self.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 b69af591fa..84744a4a33 100644 --- a/worlds/terraria/docs/setup_en.md +++ b/worlds/terraria/docs/setup_en.md @@ -31,8 +31,6 @@ 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 7c2e6deda5..e443c9b953 100644 --- a/worlds/tloz/docs/en_The Legend of Zelda.md +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -35,17 +35,9 @@ 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. -- - -## 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 +- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index df857f16df..ae53d953b1 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -6,7 +6,6 @@ - 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 9e784a4a59..5e36344703 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 87011ee16b..3905d3bc3e 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -42,22 +42,11 @@ 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. - -## 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 +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 diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md index f08902535d..18474a4269 100644 --- a/worlds/wargroove/docs/en_Wargroove.md +++ b/worlds/wargroove/docs/en_Wargroove.md @@ -26,16 +26,9 @@ 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 8a9dab54bc..4fd0edc429 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(player) - if location.address + location.name for location in multiworld.get_locations() + if location.player == player and location.address } always_locations = [ diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index a5e1bfe1ad..1e79f4f133 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -329,22 +329,23 @@ 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(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(): + 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) + ) # 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 06a11b7d79..b5d37cc202 100644 --- a/worlds/zillion/docs/en_Zillion.md +++ b/worlds/zillion/docs/en_Zillion.md @@ -67,16 +67,8 @@ 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. From b8948bc4958855c6e342e18bdb8dc81cfcf09455 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:38:45 -0500 Subject: [PATCH 5/8] fix things --- .gitignore | 3 + AHITClient.py | 236 +++++++ data/yatta.ico | Bin 0 -> 152484 bytes data/yatta.png | Bin 0 -> 34873 bytes setup-ahitclient.py | 642 ++++++++++++++++++ worlds/ahit/DeathWishLocations.py | 262 +++++++ worlds/ahit/DeathWishRules.py | 539 +++++++++++++++ worlds/ahit/Items.py | 286 ++++++++ worlds/ahit/Locations.py | 977 +++++++++++++++++++++++++++ worlds/ahit/Options.py | 728 ++++++++++++++++++++ worlds/ahit/Regions.py | 900 ++++++++++++++++++++++++ worlds/ahit/Rules.py | 944 ++++++++++++++++++++++++++ worlds/ahit/Types.py | 80 +++ worlds/ahit/__init__.py | 334 +++++++++ worlds/ahit/docs/en_A Hat in Time.md | 31 + worlds/ahit/docs/setup_en.md | 43 ++ worlds/ahit/test/TestActs.py | 31 + worlds/ahit/test/TestBase.py | 5 + worlds/ahit/test/__init__.py | 0 19 files changed, 6041 insertions(+) create mode 100644 AHITClient.py create mode 100644 data/yatta.ico create mode 100644 data/yatta.png create mode 100644 setup-ahitclient.py create mode 100644 worlds/ahit/DeathWishLocations.py create mode 100644 worlds/ahit/DeathWishRules.py create mode 100644 worlds/ahit/Items.py create mode 100644 worlds/ahit/Locations.py create mode 100644 worlds/ahit/Options.py create mode 100644 worlds/ahit/Regions.py create mode 100644 worlds/ahit/Rules.py create mode 100644 worlds/ahit/Types.py create mode 100644 worlds/ahit/__init__.py create mode 100644 worlds/ahit/docs/en_A Hat in Time.md create mode 100644 worlds/ahit/docs/setup_en.md create mode 100644 worlds/ahit/test/TestActs.py create mode 100644 worlds/ahit/test/TestBase.py create mode 100644 worlds/ahit/test/__init__.py diff --git a/.gitignore b/.gitignore index f4bcd35c32..b9ca4b8d28 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage +/oot/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -196,3 +197,5 @@ minecraft_versions.json .LSOverride Thumbs.db [Dd]esktop.ini +A Hat in Time.yaml +ahit.apworld diff --git a/AHITClient.py b/AHITClient.py new file mode 100644 index 0000000000..884f3ee5c7 --- /dev/null +++ b/AHITClient.py @@ -0,0 +1,236 @@ +import asyncio +import Utils +import websockets +import functools +from copy import deepcopy +from typing import List, Any, Iterable +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from MultiServer import Endpoint +from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser + +DEBUG = False + + +class AHITJSONToTextParser(JSONtoTextParser): + def _handle_color(self, node: JSONMessagePart): + return self._handle_text(node) # No colors for the in-game text + + +class AHITCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_ahit(self): + """Check AHIT Connection State""" + if isinstance(self.ctx, AHITContext): + logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}") + + +class AHITContext(CommonContext): + command_processor = AHITCommandProcessor + game = "A Hat in Time" + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.proxy = None + self.proxy_task = None + self.gamejsontotext = AHITJSONToTextParser(self) + self.autoreconnect_task = None + self.endpoint = None + self.items_handling = 0b111 + self.room_info = None + self.connected_msg = None + self.game_connected = False + self.awaiting_info = False + self.full_inventory: List[Any] = [] + self.server_msgs: List[Any] = [] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AHITContext, self).server_auth(password_requested) + + await self.get_username() + await self.send_connect() + + def get_ahit_status(self) -> str: + if not self.is_proxy_connected(): + return "Not connected to A Hat in Time" + + return "Connected to A Hat in Time" + + async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool: + """ `msgs` JSON serializable """ + if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: + return False + + if DEBUG: + logger.info(f"Outgoing message: {msgs}") + + await self.endpoint.socket.send(msgs) + return True + + async def disconnect(self, allow_autoreconnect: bool = False): + await super().disconnect(allow_autoreconnect) + + async def disconnect_proxy(self): + if self.endpoint and not self.endpoint.socket.closed: + await self.endpoint.socket.close() + if self.proxy_task is not None: + await self.proxy_task + + def is_connected(self) -> bool: + return self.server and self.server.socket.open + + def is_proxy_connected(self) -> bool: + return self.endpoint and self.endpoint.socket.open + + def on_print_json(self, args: dict): + text = self.gamejsontotext(deepcopy(args["data"])) + msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"} + self.server_msgs.append(encode([msg])) + + if self.ui: + self.ui.print_json(args["data"]) + else: + text = self.jsontotextparser(args["data"]) + logger.info(text) + + def update_items(self): + # just to be safe - we might still have an inventory from a different room + if not self.is_connected(): + return + + self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.connected_msg = encode([args]) + if self.awaiting_info: + self.server_msgs.append(self.room_info) + self.update_items() + self.awaiting_info = False + + elif cmd == "ReceivedItems": + if args["index"] == 0: + self.full_inventory.clear() + + for item in args["items"]: + self.full_inventory.append(NetworkItem(*item)) + + self.server_msgs.append(encode([args])) + + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.room_info = encode([args]) + + else: + if cmd != "PrintJSON": + self.server_msgs.append(encode([args])) + + def run_gui(self): + from kvui import GameManager + + class AHITManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago A Hat in Time Client" + + self.ui = AHITManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def proxy(websocket, path: str = "/", ctx: AHITContext = None): + ctx.endpoint = Endpoint(websocket) + try: + await on_client_connected(ctx) + + if ctx.is_proxy_connected(): + async for data in websocket: + if DEBUG: + logger.info(f"Incoming message: {data}") + + for msg in decode(data): + if msg["cmd"] == "Connect": + # Proxy is connecting, make sure it is valid + if msg["game"] != "A Hat in Time": + logger.info("Aborting proxy connection: game is not A Hat in Time") + await ctx.disconnect_proxy() + break + + if ctx.seed_name: + seed_name = msg.get("seed_name", "") + if seed_name != "" and seed_name != ctx.seed_name: + logger.info("Aborting proxy connection: seed mismatch from save file") + logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - save file to seed mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + + if ctx.connected_msg and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.connected_msg) + ctx.update_items() + continue + + if not ctx.is_proxy_connected(): + break + + await ctx.send_msgs([msg]) + + except Exception as e: + if not isinstance(e, websockets.WebSocketException): + logger.exception(e) + finally: + await ctx.disconnect_proxy() + + +async def on_client_connected(ctx: AHITContext): + if ctx.room_info and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.room_info) + else: + ctx.awaiting_info = True + + +async def main(): + parser = get_base_parser() + args = parser.parse_args() + + ctx = AHITContext(args.connect, args.password) + logger.info("Starting A Hat in Time proxy server") + ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), + host="localhost", port=11311, ping_timeout=999999, ping_interval=999999) + ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.proxy + await ctx.proxy_task + await ctx.exit_event.wait() + + +async def proxy_loop(ctx: AHITContext): + try: + while not ctx.exit_event.is_set(): + if len(ctx.server_msgs) > 0: + for msg in ctx.server_msgs: + await ctx.send_msgs_proxy(msg) + + ctx.server_msgs.clear() + await asyncio.sleep(0.1) + except Exception as e: + logger.exception(e) + logger.info("Aborting AHIT Proxy Client due to errors") + + +if __name__ == '__main__': + Utils.init_logging("AHITClient") + options = Utils.get_options() + + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/data/yatta.ico b/data/yatta.ico new file mode 100644 index 0000000000000000000000000000000000000000..f87a0980f49c3cf346af8c288bab020eb77885c6 GIT binary patch literal 152484 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 HCB^>%Zp1Qi literal 0 HcmV?d00001 diff --git a/data/yatta.png b/data/yatta.png new file mode 100644 index 0000000000000000000000000000000000000000..4f230c86c727f4950a5c6e6b62b708b4d3d12f8b GIT binary patch literal 34873 zcmV)&K#aeMP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/setup-ahitclient.py b/setup-ahitclient.py new file mode 100644 index 0000000000..18fd6a1887 --- /dev/null +++ b/setup-ahitclient.py @@ -0,0 +1,642 @@ +import base64 +import datetime +import os +import platform +import shutil +import sys +import sysconfig +import typing +import warnings +import zipfile +import urllib.request +import io +import json +import threading +import subprocess + +from collections.abc import Iterable +from hashlib import sha3_512 +from pathlib import Path + + +# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it +try: + requirement = 'cx-Freeze>=6.15.2' + import pkg_resources + try: + pkg_resources.require(requirement) + install_cx_freeze = False + except pkg_resources.ResolutionError: + install_cx_freeze = True +except ImportError: + install_cx_freeze = True + pkg_resources = None # type: ignore [assignment] + +if install_cx_freeze: + # check if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + # install and import cx_freeze + if '--yes' not in sys.argv and '-y' not in sys.argv: + input(f'Requirement {requirement} is not satisfied, press enter to install it') + subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) + import pkg_resources + +import cx_Freeze + +# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line +import setuptools.command.build + +if __name__ == "__main__": + # need to run this early to import from Utils and Launcher + # TODO: move stuff to not require this + import ModuleUpdate + ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) + ModuleUpdate.update_ran = False # restore for later + +from worlds.LauncherComponents import components, icon_paths +from Utils import version_tuple, is_windows, is_linux +from Cython.Build import cythonize + + +# On Python < 3.10 LogicMixin is not currently supported. +non_apworlds: set = { + "A Link to the Past", + "Adventure", + "ArchipIDLE", + "Archipelago", + "ChecksFinder", + "Clique", + "DLCQuest", + "Final Fantasy", + "Hylics 2", + "Kingdom Hearts 2", + "Lufia II Ancient Cave", + "Meritous", + "Ocarina of Time", + "Overcooked! 2", + "Raft", + "Secret of Evermore", + "Slay the Spire", + "Starcraft 2 Wings of Liberty", + "Sudoku", + "Super Mario 64", + "VVVVVV", + "Wargroove", + "Zillion", +} + +# LogicMixin is broken before 3.10 import revamp +if sys.version_info < (3,10): + non_apworlds.add("Hollow Knight") + +def download_SNI(): + print("Updating SNI") + machine_to_go = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm" + } + platform_name = platform.system().lower() + machine_name = platform.machine().lower() + # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH + machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: + data = json.load(request) + files = data["assets"] + + source_url = None + + for file in files: + download_url: str = file["browser_download_url"] + machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name + if platform_name in download_url and machine_match: + # prefer "many" builds + if "many" in download_url: + source_url = download_url + break + source_url = download_url + + if source_url and source_url.endswith(".zip"): + with urllib.request.urlopen(source_url) as download: + with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + zf.extract(member, path="SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): + import tarfile + mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" + with urllib.request.urlopen(source_url) as download: + sni_dir = None + with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: + for member in tf.getmembers(): + if member.name.startswith("/") or "../" in member.name: + raise ValueError(f"Unexpected file '{member.name}' in {source_url}") + elif member.isdir() and not sni_dir: + sni_dir = member.name + elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + raise ValueError(f"Expected folder before '{member.name}' in {source_url}") + elif member.isfile() and sni_dir: + tf.extract(member) + # sadly SNI is in its own folder on non-windows, so we need to rename + shutil.rmtree("SNI", True) + os.rename(sni_dir, "SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url: + print(f"Don't know how to extract SNI from {source_url}") + + else: + print(f"No SNI found for system spec {platform_name} {machine_name}") + + +signtool: typing.Optional[str] +if os.path.exists("X:/pw.txt"): + print("Using signtool") + with open("X:/pw.txt", encoding="utf-8-sig") as f: + pw = f.read() + signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \ + r'" /fd sha256 /tr http://timestamp.digicert.com/ ' +else: + signtool = None + + +build_platform = sysconfig.get_platform() +arch_folder = "exe.{platform}-{version}".format(platform=build_platform, + version=sysconfig.get_python_version()) +buildfolder = Path("build", arch_folder) +build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine() + + +# see Launcher.py on how to add scripts to setup.py +def resolve_icon(icon_name: str): + base_path = icon_paths[icon_name] + if is_windows: + path, extension = os.path.splitext(base_path) + ico_file = path + ".ico" + assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist." + return ico_file + else: + return base_path + + +exes = [ + cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name="ArchipelagoAHITClient.exe", + #target_name=c.frozen_name + (".exe" if is_windows else ""), + icon=resolve_icon(c.icon), + base="Win32GUI" if is_windows and not c.cli else None + ) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name +] + +#if is_windows: +if False: + # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help + c = next(component for component in components if component.script_name == "Launcher") + exes.append(cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name=f"{c.frozen_name}(DEBUG).exe", + icon=resolve_icon(c.icon), + )) + +extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] +extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] + + +def remove_sprites_from_folder(folder): + for file in os.listdir(folder): + if file != ".gitignore": + os.remove(folder / file) + + +def _threaded_hash(filepath): + hasher = sha3_512() + hasher.update(open(filepath, "rb").read()) + return base64.b85encode(hasher.digest()).decode() + + +# cx_Freeze's build command runs other commands. Override to accept --yes and store that. +class BuildCommand(setuptools.command.build.build): + user_options = [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ] + yes: bool + last_yes: bool = False # used by sub commands of build + + def initialize_options(self): + super().initialize_options() + type(self).last_yes = self.yes = False + + def finalize_options(self): + super().finalize_options() + type(self).last_yes = self.yes + + +# Override cx_Freeze's build_exe command for pre and post build steps +class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): + user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ('extra-data=', None, 'Additional files to add.'), + ] + yes: bool + extra_data: Iterable # [any] not available in 3.8 + extra_libs: Iterable # work around broken include_files + + buildfolder: Path + libfolder: Path + library: Path + buildtime: datetime.datetime + + def initialize_options(self): + super().initialize_options() + self.yes = BuildCommand.last_yes + self.extra_data = [] + self.extra_libs = [] + + def finalize_options(self): + super().finalize_options() + self.buildfolder = self.build_exe + self.libfolder = Path(self.buildfolder, "lib") + self.library = Path(self.libfolder, "library.zip") + + def installfile(self, path, subpath=None, keep_content: bool = False): + folder = self.buildfolder + if subpath: + folder /= subpath + print('copying', path, '->', folder) + if path.is_dir(): + folder /= path.name + if folder.is_dir() and not keep_content: + shutil.rmtree(folder) + shutil.copytree(path, folder, dirs_exist_ok=True) + elif path.is_file(): + shutil.copy(path, folder) + else: + print('Warning,', path, 'not found') + + def create_manifest(self, create_hashes=False): + # Since the setup is now split into components and the manifest is not, + # it makes most sense to just remove the hashes for now. Not aware of anyone using them. + hashes = {} + manifestpath = os.path.join(self.buildfolder, "manifest.json") + if create_hashes: + from concurrent.futures import ThreadPoolExecutor + pool = ThreadPoolExecutor() + for dirpath, dirnames, filenames in os.walk(self.buildfolder): + for filename in filenames: + path = os.path.join(dirpath, filename) + hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path) + + import json + manifest = { + "buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"), + "hashes": {path: hash.result() for path, hash in hashes.items()}, + "version": version_tuple} + + json.dump(manifest, open(manifestpath, "wt"), indent=4) + print("Created Manifest") + + def run(self): + # start downloading sni asap + sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") + sni_thread.start() + + # pre-build steps + print(f"Outputting to: {self.buildfolder}") + os.makedirs(self.buildfolder, exist_ok=True) + import ModuleUpdate + ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) + ModuleUpdate.update(yes=self.yes) + + # auto-build cython modules + build_ext = self.distribution.get_command_obj("build_ext") + build_ext.inplace = False + self.run_command("build_ext") + # find remains of previous in-place builds, try to delete and warn otherwise + for path in build_ext.get_outputs(): + parts = os.path.split(path)[-1].split(".") + pattern = parts[0] + ".*." + parts[-1] + for match in Path().glob(pattern): + try: + match.unlink() + print(f"Removed {match}") + except Exception as ex: + warnings.warn(f"Could not delete old build output: {match}\n" + f"{ex}\nPlease close all AP instances and delete manually.") + + # regular cx build + self.buildtime = datetime.datetime.utcnow() + super().run() + + # manually copy built modules to lib folder. cx_Freeze does not know they exist. + for src in build_ext.get_outputs(): + print(f"copying {src} -> {self.libfolder}") + shutil.copy(src, self.libfolder, follow_symlinks=False) + + # need to finish download before copying + sni_thread.join() + + # include_files seems to not be done automatically. implement here + for src, dst in self.include_files: + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # now that include_files is completely broken, run find_libs here + for src, dst in find_libs(*self.extra_libs): + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # post build steps + if is_windows: # kivy_deps is win32 only, linux picks them up automatically + from kivy_deps import sdl2, glew + for folder in sdl2.dep_bins + glew.dep_bins: + shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) + print(f"copying {folder} -> {self.libfolder}") + + for data in self.extra_data: + self.installfile(Path(data)) + + # kivi data files + import kivy + shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), + self.buildfolder / "data", + dirs_exist_ok=True) + + os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True) + from Options import generate_yaml_templates + from worlds.AutoWorld import AutoWorldRegister + assert not non_apworlds - set(AutoWorldRegister.world_types), \ + f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" + folders_to_remove: typing.List[str] = [] + generate_yaml_templates(self.buildfolder / "Players" / "Templates", False) + for worldname, worldtype in AutoWorldRegister.world_types.items(): + if worldname not in non_apworlds: + file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] + world_directory = self.libfolder / "worlds" / file_name + # this method creates an apworld that cannot be moved to a different OS or minor python version, + # which should be ok + with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, + compresslevel=9) as zf: + for path in world_directory.rglob("*.*"): + relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) + zf.write(path, relative_path) + folders_to_remove.append(file_name) + shutil.rmtree(world_directory) + shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") + # TODO: fix LttP options one day + shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml") + try: + from maseya import z3pr + except ImportError: + print("Maseya Palette Shuffle not found, skipping data files.") + else: + # maseya Palette Shuffle exists and needs its data files + print("Maseya Palette Shuffle found, including data files...") + file = z3pr.__file__ + self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True) + + if signtool: + for exe in self.distribution.executables: + print(f"Signing {exe.target_name}") + os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) + print("Signing SNI") + os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) + print("Signing OoT Utils") + for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): + os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) + + remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr") + + self.create_manifest() + + if is_windows: + # Inno setup stuff + with open("setup.ini", "w") as f: + min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000" + f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n") + with open("installdelete.iss", "w") as f: + f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n" + for world_directory in folders_to_remove) + else: + # make sure extra programs are executable + enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core' + sni_exe = self.buildfolder / 'SNI/sni' + extra_exes = (enemizer_exe, sni_exe) + for extra_exe in extra_exes: + if extra_exe.is_file(): + extra_exe.chmod(0o755) + + +class AppImageCommand(setuptools.Command): + description = "build an app image from build output" + user_options = [ + ("build-folder=", None, "Folder to convert to AppImage."), + ("dist-file=", None, "AppImage output file."), + ("app-dir=", None, "Folder to use for packaging."), + ("app-icon=", None, "The icon to use for the AppImage."), + ("app-exec=", None, "The application to run inside the image."), + ("yes", "y", 'Answer "yes" to all questions.'), + ] + build_folder: typing.Optional[Path] + dist_file: typing.Optional[Path] + app_dir: typing.Optional[Path] + app_name: str + app_exec: typing.Optional[Path] + app_icon: typing.Optional[Path] # source file + app_id: str # lower case name, used for icon and .desktop + yes: bool + + def write_desktop(self): + assert self.app_dir, "Invalid app_dir" + desktop_filename = self.app_dir / f"{self.app_id}.desktop" + with open(desktop_filename, 'w', encoding="utf-8") as f: + f.write("\n".join(( + "[Desktop Entry]", + f'Name={self.app_name}', + f'Exec={self.app_exec}', + "Type=Application", + "Categories=Game", + f'Icon={self.app_id}', + '' + ))) + desktop_filename.chmod(0o755) + + def write_launcher(self, default_exe: Path): + assert self.app_dir, "Invalid app_dir" + launcher_filename = self.app_dir / "AppRun" + with open(launcher_filename, 'w', encoding="utf-8") as f: + f.write(f"""#!/bin/sh +exe="{default_exe}" +match="${{1#--executable=}}" +if [ "${{#match}}" -lt "${{#1}}" ]; then + exe="$match" + shift +elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then + exe="$2" + shift; shift +fi +tmp="${{exe#*/}}" +if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then + exe="{default_exe.parent}/$exe" +fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" +$APPDIR/$exe "$@" +""") + launcher_filename.chmod(0o755) + + def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + assert self.app_dir, "Invalid app_dir" + try: + from PIL import Image + except ModuleNotFoundError: + if not self.yes: + input("Requirement PIL is not satisfied, press enter to install it") + subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) + from PIL import Image + im = Image.open(src) + res, _ = im.size + + if not name: + name = src.stem + ext = src.suffix + dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps') + dest_dir.mkdir(parents=True, exist_ok=True) + dest_file = dest_dir / f'{name}{ext}' + shutil.copy(src, dest_file) + if symlink: + symlink.symlink_to(dest_file.relative_to(symlink.parent)) + + def initialize_options(self): + self.build_folder = None + self.app_dir = None + self.app_name = self.distribution.metadata.name + self.app_icon = self.distribution.executables[0].icon + self.app_exec = Path('opt/{app_name}/{exe}'.format( + app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name + )) + self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format( + app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version, + platform=sysconfig.get_platform() + )) + self.yes = False + + def finalize_options(self): + if not self.app_dir: + self.app_dir = self.build_folder.parent / "AppDir" + self.app_id = self.app_name.lower() + + def run(self): + self.dist_file.parent.mkdir(parents=True, exist_ok=True) + if self.app_dir.is_dir(): + shutil.rmtree(self.app_dir) + self.app_dir.mkdir(parents=True) + opt_dir = self.app_dir / "opt" / self.distribution.metadata.name + shutil.copytree(self.build_folder, opt_dir) + root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' + self.install_icon(self.app_icon, self.app_id, symlink=root_icon) + shutil.copy(root_icon, self.app_dir / '.DirIcon') + self.write_desktop() + self.write_launcher(self.app_exec) + print(f'{self.app_dir} -> {self.dist_file}') + subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) + + +def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: + """Try to find system libraries to be included.""" + if not args: + return [] + + arch = build_arch.replace('_', '-') + libc = 'libc6' # we currently don't support musl + + def parse(line): + lib, path = line.strip().split(' => ') + lib, typ = lib.split(' ', 1) + for test_arch in ('x86-64', 'i386', 'aarch64'): + if test_arch in typ: + lib_arch = test_arch + break + else: + lib_arch = '' + for test_libc in ('libc6',): + if test_libc in typ: + lib_libc = test_libc + break + else: + lib_libc = '' + return (lib, lib_arch, lib_libc), path + + if not hasattr(find_libs, "cache"): + ldconfig = shutil.which("ldconfig") + assert ldconfig, "Make sure ldconfig is in PATH" + data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] + find_libs.cache = { # type: ignore [attr-defined] + k: v for k, v in (parse(line) for line in data if "=>" in line) + } + + def find_lib(lib, arch, libc): + for k, v in find_libs.cache.items(): + if k == (lib, arch, libc): + return v + for k, v, in find_libs.cache.items(): + if k[0].startswith(lib) and k[1] == arch and k[2] == libc: + return v + return None + + res = [] + for arg in args: + # try exact match, empty libc, empty arch, empty arch and libc + file = find_lib(arg, arch, libc) + file = file or find_lib(arg, arch, '') + file = file or find_lib(arg, '', libc) + file = file or find_lib(arg, '', '') + # resolve symlinks + for n in range(0, 5): + res.append((file, os.path.join('lib', os.path.basename(file)))) + if not os.path.islink(file): + break + dirname = os.path.dirname(file) + file = os.readlink(file) + if not os.path.isabs(file): + file = os.path.join(dirname, file) + return res + + +cx_Freeze.setup( + name="Archipelago", + version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}", + description="Archipelago", + executables=exes, + ext_modules=cythonize("_speedups.pyx"), + options={ + "build_exe": { + "packages": ["worlds", "kivy", "cymem", "websockets"], + "includes": [], + "excludes": ["numpy", "Cython", "PySide2", "PIL", + "pandas"], + "zip_include_packages": ["*"], + "zip_exclude_packages": ["worlds", "sc2"], + "include_files": [], # broken in cx 6.14.0, we use more special sauce now + "include_msvcr": False, + "replace_paths": ["*."], + "optimize": 1, + "build_exe": buildfolder, + "extra_data": extra_data, + "extra_libs": extra_libs, + "bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else [] + }, + "bdist_appimage": { + "build_folder": buildfolder, + }, + }, + # override commands to get custom stuff in + cmdclass={ + "build": BuildCommand, + "build_exe": BuildExeCommand, + "bdist_appimage": AppImageCommand, + }, +) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py new file mode 100644 index 0000000000..f51d4948ee --- /dev/null +++ b/worlds/ahit/DeathWishLocations.py @@ -0,0 +1,262 @@ +from .Types import HatInTimeLocation, HatInTimeItem +from .Regions import connect_regions, create_region +from BaseClasses import Region, LocationProgressType, ItemClassification +from worlds.generic.Rules import add_rule +from worlds.AutoWorld import World +from typing import List +from .Locations import death_wishes + + +dw_prereqs = { + "So You're Back From Outer Space": ["Beat the Heat"], + "Snatcher's Hit List": ["Beat the Heat"], + "Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"], + "Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"], + "Collect-a-thon": ["So You're Back From Outer Space"], + "She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"], + "Mafia's Jumps": ["She Speedran from Outer Space"], + "Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"], + "Encore! Encore!": ["Collect-a-thon"], + + "Security Breach": ["Beat the Heat"], + "Rift Collapse: Dead Bird Studio": ["Security Breach"], + "The Great Big Hootenanny": ["Security Breach"], + "10 Seconds until Self-Destruct": ["The Great Big Hootenanny"], + "Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"], + "Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"], + "Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"], + "Zero Jumps": ["Rift Collapse: Dead Bird Studio"], + "Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"], + + "Speedrun Well": ["Beat the Heat"], + "Rift Collapse: Sleepy Subcon": ["Speedrun Well"], + "Boss Rush": ["Speedrun Well"], + "Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"], + "Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"], + "Community Rift: Twilight Travels": ["Quality Time with Snatcher"], + "Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"], + + "Bird Sanctuary": ["Beat the Heat"], + "Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"], + "Wound-Up Windmill": ["Bird Sanctuary"], + "Rift Collapse: Alpine Skyline": ["Bird Sanctuary"], + "Camera Tourist": ["Rift Collapse: Alpine Skyline"], + "Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"], + "The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"], + + "The Mustache Gauntlet": ["Wound-Up Windmill"], + "No More Bad Guys": ["The Mustache Gauntlet"], + "Seal the Deal": ["Encore! Encore!", "Killing Two Birds", + "Breaching the Contract", "No More Bad Guys"], + + "Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio", + "Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"], + + "Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"], +} + +dw_candles = [ + "Snatcher's Hit List", + "Zero Jumps", + "Camera Tourist", + "Snatcher Coins in Mafia Town", + "Snatcher Coins in Battle of the Birds", + "Snatcher Coins in Subcon Forest", + "Snatcher Coins in Alpine Skyline", + "Snatcher Coins in Nyakuza Metro", +] + +annoying_dws = [ + "Vault Codes in the Wind", + "Boss Rush", + "Camera Tourist", + "The Mustache Gauntlet", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", + "Seal the Deal", # Non-excluded if goal +] + +# includes the above as well +annoying_bonuses = [ + "So You're Back From Outer Space", + "Encore! Encore!", + "Snatcher's Hit List", + "10 Seconds until Self-Destruct", + "Killing Two Birds", + "Zero Jumps", + "Bird Sanctuary", + "Wound-Up Windmill", + "Seal the Deal", +] + +dw_classes = { + "Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder", + "So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace", + "Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody", + "Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy", + "Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown", + "Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX", + "She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien", + "Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien", + "Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault", + "Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown", + + "Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards", + "The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade", + "Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds", + "10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime", + "Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX", + "Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds", + "Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses", + + "Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell", + "Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon", + "Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush", + "Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest", + "Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX", + "Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon", + + "Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse", + "Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps", + "Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill", + "The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness", + "Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps", + "Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1", + + "The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle", + "No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX", + + "Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX", + "Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise", + "Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks", + + "Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump", + "Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels", + "Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift", + + "Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro", +} + + +def create_dw_regions(world: World): + if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0: + for name in annoying_dws: + world.get_excluded_dws().append(name) + + if world.multiworld.DWEnableBonus[world.player].value == 0 \ + or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0: + for name in death_wishes: + world.get_excluded_bonuses().append(name) + elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0: + for name in annoying_bonuses: + world.get_excluded_bonuses().append(name) + + if world.multiworld.DWExcludeCandles[world.player].value > 0: + for name in dw_candles: + if name in world.get_excluded_dws(): + continue + world.get_excluded_dws().append(name) + + spaceship = world.multiworld.get_region("Spaceship", world.player) + dw_map: Region = create_region(world, "Death Wish Map") + entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) + + add_rule(entrance, lambda state: state.has("Time Piece", world.player, + world.multiworld.DWTimePieceRequirement[world.player].value)) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list: List[str] = [] + for name in death_wishes.keys(): + if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): + continue + + dw_list.append(name) + + world.random.shuffle(dw_list) + count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + dw_shuffle: List[str] = [] + total = min(len(dw_list), count) + for i in range(total): + dw_shuffle.append(dw_list[i]) + + # Seal the Deal is always last if it's the goal + if world.multiworld.EndGoal[world.player].value == 3: + if "Seal the Deal" in dw_shuffle: + dw_shuffle.remove("Seal the Deal") + + dw_shuffle.append("Seal the Deal") + + world.set_dw_shuffle(dw_shuffle) + prev_dw: Region + for i in range(len(dw_shuffle)): + name = dw_shuffle[i] + dw = create_region(world, name) + + if i == 0: + connect_regions(dw_map, dw, f"-> {name}", world.player) + else: + connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + + loc_id = death_wishes[name] + main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", + ItemClassification.progression, None, world.player)) + + if name in world.get_excluded_dws(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(name): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) + prev_dw = dw + else: + for key, loc_id in death_wishes.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + world.get_excluded_dws().append(key) + continue + + dw = create_region(world, key) + + if key == "Beat the Heat": + connect_regions(dw_map, dw, "-> Beat the Heat", world.player) + elif key in dw_prereqs.keys(): + for name in dw_prereqs[key]: + parent = world.multiworld.get_region(name, world.player) + connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) + + main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {key}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {key}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", + ItemClassification.progression, None, world.player)) + + if key in world.get_excluded_dws(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(key): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py new file mode 100644 index 0000000000..c448484036 --- /dev/null +++ b/worlds/ahit/DeathWishRules.py @@ -0,0 +1,539 @@ +from worlds.AutoWorld import World, CollectionState +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings +from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData +from .DeathWishLocations import dw_prereqs, dw_candles +from BaseClasses import Entrance, Location, ItemClassification +from worlds.generic.Rules import add_rule, set_rule +from typing import List, Callable +from .Regions import act_chapters +from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes + +# Any speedruns expect the player to have Sprint Hat +dw_requirements = { + "Beat the Heat": LocData(umbrella=True), + "So You're Back From Outer Space": LocData(hookshot=True), + "She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), + "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), + + "Security Breach": LocData(hit_requirement=1), + "10 Seconds until Self-Destruct": LocData(hookshot=True), + "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), + + "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), + "Boss Rush": LocData(umbrella=True, hookshot=True), + "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Bird Sanctuary": LocData(hookshot=True), + "Wound-Up Windmill": LocData(hookshot=True), + "The Illness has Speedrun": LocData(hookshot=True), + "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + "Camera Tourist": LocData(misc_required=["Camera Badge"]), + + "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Rift Collapse - Deep Sea": LocData(hookshot=True), +} + +# Includes main objective requirements +dw_bonus_requirements = { + # Some One-Hit Hero requirements need badge pins as well because of Hookshot + "So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]), + + "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]), + + "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]), + "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]), + + "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), + + "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]), +} + +dw_stamp_costs = { + "So You're Back From Outer Space": 2, + "Collect-a-thon": 5, + "She Speedran from Outer Space": 8, + "Encore! Encore!": 10, + + "Security Breach": 4, + "The Great Big Hootenanny": 7, + "10 Seconds until Self-Destruct": 15, + "Killing Two Birds": 25, + "Snatcher Coins in Nyakuza Metro": 30, + + "Speedrun Well": 10, + "Boss Rush": 15, + "Quality Time with Snatcher": 20, + "Breaching the Contract": 40, + + "Bird Sanctuary": 15, + "Wound-Up Windmill": 30, + "The Illness has Speedrun": 35, + + "The Mustache Gauntlet": 35, + "No More Bad Guys": 50, + "Seal the Deal": 70, +} + +required_snatcher_coins = { + "Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower", + "Snatcher Coin - Under Ruined Tower"], + + "Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush", + "Snatcher Coin - Picture Perfect"], + + "Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof", + "Snatcher Coin - Giant Time Piece"], + + "Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake", + "Snatcher Coin - Windmill"], + + "Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train", + "Snatcher Coin - Pink Paw Fence"], +} + + +def set_dw_rules(world: World): + if "Snatcher's Hit List" not in world.get_excluded_dws() \ + or "Camera Tourist" not in world.get_excluded_dws(): + set_enemy_rules(world) + + dw_list: List[str] = [] + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list = world.get_dw_shuffle() + else: + for name in death_wishes.keys(): + dw_list.append(name) + + for name in dw_list: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + dw = world.multiworld.get_region(name, world.player) + temp_list: List[Location] = [] + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) + bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) + temp_list.append(main_objective) + temp_list.append(full_clear) + + if world.multiworld.DWShuffle[world.player].value == 0: + if name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + + if world.multiworld.DWEnableBonus[world.player].value == 0: + # place nothing, but let the locations exist still, so we can use them for bonus stamp rules + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) + full_clear.show_in_spoiler = False + + # No need for rules if excluded - stamps will be auto-granted + if world.is_dw_excluded(name): + continue + + # Specific Rules + modify_dw_rules(world, name) + + main_rule: Callable[[CollectionState], bool] + for i in range(len(temp_list)): + loc = temp_list[i] + data: LocData + + if loc.name == main_objective.name: + data = dw_requirements.get(name) + else: + data = dw_bonus_requirements.get(name) + + if data is None: + continue + + if data.hookshot: + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + + for misc in data.misc_required: + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(loc, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + main_rule = main_objective.access_rule + + if loc.name == main_objective.name: + add_rule(main_stamp, loc.access_rule) + elif loc.name == full_clear.name: + add_rule(loc, main_rule) + # Only set bonus stamp rules if we don't auto complete bonuses + if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \ + and not world.is_bonus_excluded(loc.name): + add_rule(bonus_stamps, loc.access_rule) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_shuffle = world.get_dw_shuffle() + for i in range(len(dw_shuffle)): + if i == 0: + continue + + name = dw_shuffle[i] + prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player) + entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) + add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) + else: + for key, reqs in dw_prereqs.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + access_rules: List[Callable[[CollectionState], bool]] = [] + entrances: List[Entrance] = [] + + for parent in reqs: + entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player) + entrances.append(entrance) + + if not world.is_dw_excluded(parent): + access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player)) + + for entrance in entrances: + for rule in access_rules: + add_rule(entrance, rule) + + if world.multiworld.EndGoal[world.player].value == 3: + world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", + world.player) + + +def modify_dw_rules(world: World, name: str): + difficulty: Difficulty = get_difficulty(world) + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "The Illness has Speedrun": + # All stamps with hookshot only in Expert + if difficulty >= Difficulty.EXPERT: + set_rule(full_clear, lambda state: True) + else: + add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) + + elif name == "The Mustache Gauntlet": + add_rule(main_objective, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING)) + + elif name == "Vault Codes in the Wind": + # Sprint is normally expected here + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Speedrun Well": + # All stamps with nothing :) + if difficulty >= Difficulty.EXPERT: + set_rule(main_objective, lambda state: True) + + elif name == "Mafia's Jumps": + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + set_rule(full_clear, lambda state: True) + + elif name == "So You're Back from Outer Space": + # Without Hookshot + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Wound-Up Windmill": + # No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it. + if difficulty >= Difficulty.MODERATE: + set_rule(full_clear, lambda state: can_use_hookshot(state, world) + and state.has("One-Hit Hero Badge", world.player)) + + if name in dw_candles: + set_candle_dw_rules(name, world) + + +def get_total_dw_stamps(state: CollectionState, world: World) -> int: + if world.multiworld.DWShuffle[world.player].value > 0: + return 999 # no stamp costs in death wish shuffle + + count: int = 0 + + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + if state.has(f"1 Stamp - {name}", world.player): + count += 1 + else: + continue + + if state.has(f"2 Stamps - {name}", world.player): + count += 2 + elif name not in dw_candles: + # most non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) + count += 1 + + return count + + +def set_candle_dw_rules(name: str, world: World): + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "Zero Jumps": + add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) + add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 + and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE)) + + # No Ice Hat/painting required in Expert for Toilet Zero Jumps + # This painting wall can only be skipped via cherry hover. + if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + else: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world)) + + set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player), + lambda state: has_paintings(state, world, 1, False)) + + elif name == "Snatcher's Hit List": + add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) + add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12) + + elif name == "Camera Tourist": + add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) + add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) + and state.has("Triple Enemy Photo", world.player)) + + elif "Snatcher Coins" in name: + for coin in required_snatcher_coins[name]: + add_rule(main_objective, lambda state: state.has(coin, world.player), "or") + add_rule(full_clear, lambda state: state.has(coin, world.player)) + + +def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: + total: int = 0 + + for name in act_chapters.keys(): + n = f"{name} (Zero Jumps)" + if n not in zero_jumps: + continue + + if get_difficulty(world) < Difficulty.HARD and n in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and n in zero_jumps_expert: + continue + + if not state.has(n, world.player): + continue + + total += 1 + + return total + + +def get_reachable_enemy_count(state: CollectionState, world: World) -> int: + count: int = 0 + for enemy in hit_list.keys(): + if enemy in bosses: + continue + + if state.has(enemy, world.player): + count += 1 + + return count + + +def can_reach_all_bosses(state: CollectionState, world: World) -> bool: + for boss in bosses: + if not state.has(boss, world.player): + return False + + return True + + +def create_enemy_events(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes.keys() \ + and area not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(area, world.player) + event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region) + event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + + for name in triple_enemy_locations: + if name == "Time Rift - Tour" and (not world.is_dlc1() or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and name in death_wishes.keys() \ + and name not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(name, world.player) + event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + if name == "The Mustache Gauntlet": + add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + +def set_enemy_rules(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + and area not in world.get_dw_shuffle(): + continue + + event = world.multiworld.get_location(f"{enemy} - {area}", world.player) + + if enemy == "Toxic Flower": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + if area == "The Illness has Spread": + add_rule(event, lambda state: not zipline_logic(world) or + state.has("Zipline Unlock - The Birdhouse Path", world.player) + or state.has("Zipline Unlock - The Lava Cake Path", world.player) + or state.has("Zipline Unlock - The Windmill Path", world.player)) + + elif enemy == "Director": + if area == "Dead Bird Studio Basement": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + elif enemy == "Snatcher" or enemy == "Mustache Girl": + if area == "Boss Rush": + # need to be able to kill toilet and snatcher + add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world)) + if enemy == "Mustache Girl": + add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world)) + + elif area == "The Finale" and enemy == "Mustache Girl": + add_rule(event, lambda state: can_use_hookshot(state, world) + and can_use_hat(state, world, HatType.DWELLER)) + + elif enemy == "Shock Squid" or enemy == "Ninja Cat": + if area == "Time Rift - Deep Sea": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + +# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them +hit_list = { + "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour", + "Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks", + "So You're Back From Outer Space"], + + "Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell", + "She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet", + "Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"], + + "UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"], + + "Rat": ["Down with the Mafia!", "Bluefin Tunnel"], + + "Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea", + "Rift Collapse: Sleepy Subcon"], + + "Shromb Egg": ["The Birdhouse", "Bird Sanctuary"], + + "Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well", + "The Lava Cake", "The Windmill"], + + "Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary", + "Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"], + + "Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"], + + "Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"], + + "Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"], + + "Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet", + "Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea", + "Rift Collapse: Alpine Skyline"], + + # Bosses + "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], + + "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], + "Toilet": ["Toilet of Doom", "Boss Rush"], + + "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", + "Quality Time with Snatcher"], + + "Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"], + + "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], +} + +# Camera Tourist has a bonus that requires getting three different types of enemies in one picture. +triple_enemy_locations = [ + "She Came from Outer Space", + "She Speedran from Outer Space", + "Mafia's Jumps", + "The Mustache Gauntlet", + "The Birdhouse", + "Bird Sanctuary", + "Time Rift - Tour", +] + +bosses = [ + "Mafia Boss", + "Conductor", + "Toilet", + "Snatcher", + "Toxic Flower", + "Mustache Girl", +] diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py new file mode 100644 index 0000000000..869f998a9d --- /dev/null +++ b/worlds/ahit/Items.py @@ -0,0 +1,286 @@ +from BaseClasses import Item, ItemClassification +from worlds.AutoWorld import World +from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem +from .Locations import get_total_locations +from .Rules import get_difficulty +from typing import Optional, List, Dict + + +def create_itempool(world: World) -> List[Item]: + itempool: List[Item] = [] + if not world.is_dw_only() and world.multiworld.HatItems[world.player].value == 0: + calculate_yarn_costs(world) + yarn_pool: List[Item] = create_multiple_items(world, "Yarn", + world.multiworld.YarnAvailable[world.player].value, + ItemClassification.progression_skip_balancing) + + for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))): + yarn_pool[i].classification = ItemClassification.progression + + itempool += yarn_pool + + for name in item_table.keys(): + if name == "Yarn": + continue + + if not item_dlc_enabled(world, name): + continue + + if world.multiworld.HatItems[world.player].value == 0 and name in hat_type_to_item.values(): + continue + + item_type: ItemClassification = item_table.get(name).classification + + if world.is_dw_only(): + if item_type is ItemClassification.progression \ + or item_type is ItemClassification.progression_skip_balancing: + continue + else: + if name == "Scooter Badge": + if world.multiworld.CTRLogic[world.player].value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + elif name == "No Bonk Badge": + if get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + + # some death wish bonuses require one hit hero + hookshot + if world.is_dw() and name == "Badge Pin" and not world.is_dw_only(): + item_type = ItemClassification.progression + + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0: + continue + + if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0: + continue + + if name == "Progressive Painting Unlock" \ + and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + continue + + if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": + continue + + if name == "Time Piece": + tp_count: int = 40 + max_extra: int = 0 + if world.is_dlc1(): + max_extra += 6 + + if world.is_dlc2(): + max_extra += 10 + + tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value) + tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) + + for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))): + tp_list[i].classification = ItemClassification.progression + + itempool += tp_list + continue + + itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type) + + itempool += create_junk_items(world, get_total_locations(world) - len(itempool)) + return itempool + + +def calculate_yarn_costs(world: World): + mw = world.multiworld + p = world.player + min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + + max_cost: int = 0 + for i in range(5): + cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + world.get_hat_yarn_costs()[HatType(i)] = cost + max_cost += cost + + available_yarn: int = mw.YarnAvailable[p].value + if max_cost > available_yarn: + mw.YarnAvailable[p].value = max_cost + available_yarn = max_cost + + if max_cost + mw.MinExtraYarn[p].value > available_yarn: + mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn + + +def item_dlc_enabled(world: World, name: str) -> bool: + data = item_table[name] + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): + return True + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): + return True + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): + return True + + return False + + +def create_item(world: World, name: str) -> Item: + data = item_table[name] + return HatInTimeItem(name, data.classification, data.code, world.player) + + +def create_multiple_items(world: World, name: str, count: int = 1, + item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: + + data = item_table[name] + itemlist: List[Item] = [] + + for i in range(count): + itemlist += [HatInTimeItem(name, item_type, data.code, world.player)] + + return itemlist + + +def create_junk_items(world: World, count: int) -> List[Item]: + trap_chance = world.multiworld.TrapChance[world.player].value + junk_pool: List[Item] = [] + junk_list: Dict[str, int] = {} + trap_list: Dict[str, int] = {} + ic: ItemClassification + + for name in item_table.keys(): + ic = item_table[name].classification + if ic == ItemClassification.filler: + if world.is_dw_only() and "Pons" in name: + continue + + junk_list[name] = junk_weights.get(name) + + elif trap_chance > 0 and ic == ItemClassification.trap: + if name == "Baby Trap": + trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value + elif name == "Laser Trap": + trap_list[name] = world.multiworld.LaserTrapWeight[world.player].value + elif name == "Parade Trap": + trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value + + for i in range(count): + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: + junk_pool += [world.create_item( + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + else: + junk_pool += [world.create_item( + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + + return junk_pool + + +ahit_items = { + "Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing), + "Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing), + + # for HatItems option + "Sprint Hat": ItemData(2000300049, ItemClassification.progression), + "Brewing Hat": ItemData(2000300050, ItemClassification.progression), + "Ice Hat": ItemData(2000300051, ItemClassification.progression), + "Dweller Mask": ItemData(2000300052, ItemClassification.progression), + "Time Stop Hat": ItemData(2000300053, ItemClassification.progression), + + # Relics + "Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression), + "Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression), + "Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression), + "Relic (Train)": ItemData(2000300009, ItemClassification.progression), + "Relic (UFO)": ItemData(2000300010, ItemClassification.progression), + "Relic (Cow)": ItemData(2000300011, ItemClassification.progression), + "Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression), + "Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression), + "Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression), + "Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression), + "Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression), + "Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression), + + # Badges + "Projectile Badge": ItemData(2000300024, ItemClassification.useful), + "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful), + "Hover Badge": ItemData(2000300026, ItemClassification.useful), + "Hookshot Badge": ItemData(2000300027, ItemClassification.progression), + "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful), + "No Bonk Badge": ItemData(2000300029, ItemClassification.useful), + "Compass Badge": ItemData(2000300030, ItemClassification.useful), + "Scooter Badge": ItemData(2000300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish), + + # Other + "Badge Pin": ItemData(2000300043, ItemClassification.useful), + "Umbrella": ItemData(2000300033, ItemClassification.progression), + "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression), + + # Garbage items + "25 Pons": ItemData(2000300034, ItemClassification.filler), + "50 Pons": ItemData(2000300035, ItemClassification.filler), + "100 Pons": ItemData(2000300036, ItemClassification.filler), + "Health Pon": ItemData(2000300037, ItemClassification.filler), + "Random Cosmetic": ItemData(2000300044, ItemClassification.filler), + + # Traps + "Baby Trap": ItemData(2000300039, ItemClassification.trap), + "Laser Trap": ItemData(2000300040, ItemClassification.trap), + "Parade Trap": ItemData(2000300041, ItemClassification.trap), + + # DLC1 items + "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), + + # DLC2 items + "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2), +} + +act_contracts = { + "Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression), + "Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression), + "Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression), + "Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression), +} + +alps_hooks = { + "Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression), + "Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression), + "Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression), + "Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression), +} + +relic_groups = { + "Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"}, + "Train": {"Relic (Mountain Set)", "Relic (Train)"}, + "UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"}, + "Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"}, + "Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"}, + "Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"}, +} + +item_frequencies = { + "Badge Pin": 2, + "Progressive Painting Unlock": 3, +} + +junk_weights = { + "25 Pons": 50, + "50 Pons": 25, + "100 Pons": 10, + "Health Pon": 35, + "Random Cosmetic": 35, +} + +item_table = { + **ahit_items, + **act_contracts, + **alps_hooks, +} diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py new file mode 100644 index 0000000000..bf31c8cba8 --- /dev/null +++ b/worlds/ahit/Locations.py @@ -0,0 +1,977 @@ +from worlds.AutoWorld import World +from .Types import HatDLC, HatType, LocData, Difficulty +from typing import Dict +from .Options import TasksanityCheckCount + + +TASKSANITY_START_ID = 2000300204 + + +def get_total_locations(world: World) -> int: + total: int = 0 + + if not world.is_dw_only(): + for (name) in location_table.keys(): + if is_location_valid(world, name): + total += 1 + + if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: + total += world.multiworld.TasksanityCheckCount[world.player].value + + if world.is_dw(): + if world.multiworld.DWShuffle[world.player].value > 0: + total += len(world.get_dw_shuffle()) + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += len(world.get_dw_shuffle()) + else: + total += 37 + if world.is_dlc2(): + total += 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += 37 + if world.is_dlc2(): + total += 1 + + return total + + +def location_dlc_enabled(world: World, location: str) -> bool: + data = location_table.get(location) or event_locs.get(location) + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): + return True + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): + return True + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): + return True + elif data.dlc_flags == HatDLC.dlc1_dw and world.is_dlc1() and world.is_dw(): + return True + elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw(): + return True + + return False + + +def is_location_valid(world: World, location: str) -> bool: + if not location_dlc_enabled(world, location): + return False + + if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \ + and location in storybook_pages.keys(): + return False + + if world.multiworld.ShuffleActContracts[world.player].value == 0 \ + and location in contract_locations.keys(): + return False + + if location not in world.shop_locs and location in shop_locations: + return False + + data = location_table.get(location) or event_locs.get(location) + if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour": + return False + + # No need for all those event items if we're not doing candles + if data.dlc_flags & HatDLC.death_wish: + if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys(): + return False + + if world.multiworld.DWShuffle[world.player].value > 0 \ + and data.region in death_wishes and data.region not in world.get_dw_shuffle(): + return False + + if location in zero_jumps: + if world.multiworld.DWShuffle[world.player].value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): + return False + + difficulty: int = world.multiworld.LogicDifficulty[world.player].value + if location in zero_jumps_hard and difficulty < int(Difficulty.HARD): + return False + + if location in zero_jumps_expert and difficulty < int(Difficulty.EXPERT): + return False + + return True + + +def get_location_names() -> Dict[str, int]: + names = {name: data.id for name, data in location_table.items()} + id_start: int = TASKSANITY_START_ID + for i in range(TasksanityCheckCount.range_end): + names.setdefault(f"Tasksanity Check {i+1}", id_start+i) + + for (key, loc_id) in death_wishes.items(): + names.setdefault(f"{key} - Main Objective", loc_id) + names.setdefault(f"{key} - All Clear", loc_id+1) + + return names + + +ahit_locations = { + "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), + + # 300000 range - Mafia Town/Batle of the Birds + "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"), + "Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"), + "Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"), + "Mafia Town - Blue Vault": LocData(2000302850, "Mafia Town Area"), + "Mafia Town - Green Vault": LocData(2000302851, "Mafia Town Area"), + "Mafia Town - Red Vault": LocData(2000302848, "Mafia Town Area"), + "Mafia Town - Blue Vault Brewing Crate": LocData(2000305572, "Mafia Town Area", required_hats=[HatType.BREWING]), + "Mafia Town - Plaza Under Boxes": LocData(2000304458, "Mafia Town Area"), + "Mafia Town - Small Boat": LocData(2000304460, "Mafia Town Area"), + "Mafia Town - Staircase Pon Cluster": LocData(2000304611, "Mafia Town Area"), + "Mafia Town - Palm Tree": LocData(2000304609, "Mafia Town Area"), + "Mafia Town - Port": LocData(2000305219, "Mafia Town Area"), + "Mafia Town - Docks Chest": LocData(2000303534, "Mafia Town Area"), + "Mafia Town - Ice Hat Cage": LocData(2000304831, "Mafia Town Area", required_hats=[HatType.ICE]), + "Mafia Town - Hidden Buttons Chest": LocData(2000303483, "Mafia Town Area"), + + # These can be accessed from HUMT, the above locations can't be + "Mafia Town - Dweller Boxes": LocData(2000304462, "Mafia Town Area (HUMT)"), + "Mafia Town - Ledge Chest": LocData(2000303530, "Mafia Town Area (HUMT)"), + "Mafia Town - Yellow Sphere Building Chest": LocData(2000303535, "Mafia Town Area (HUMT)"), + "Mafia Town - Beneath Scaffolding": LocData(2000304456, "Mafia Town Area (HUMT)"), + "Mafia Town - On Scaffolding": LocData(2000304457, "Mafia Town Area (HUMT)"), + "Mafia Town - Cargo Ship": LocData(2000304459, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Alcove": LocData(2000304463, "Mafia Town Area (HUMT)"), + "Mafia Town - Wood Cage": LocData(2000304606, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Patio": LocData(2000304610, "Mafia Town Area (HUMT)"), + "Mafia Town - Steel Beam Nest": LocData(2000304608, "Mafia Town Area (HUMT)"), + "Mafia Town - Top of Ruined Tower": LocData(2000304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Hot Air Balloon": LocData(2000304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Camera Badge 1": LocData(2000302003, "Mafia Town Area (HUMT)"), + "Mafia Town - Camera Badge 2": LocData(2000302004, "Mafia Town Area (HUMT)"), + "Mafia Town - Chest Beneath Aqueduct": LocData(2000303489, "Mafia Town Area (HUMT)"), + "Mafia Town - Secret Cave": LocData(2000305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]), + "Mafia Town - Crow Chest": LocData(2000303532, "Mafia Town Area (HUMT)"), + "Mafia Town - Above Boats": LocData(2000305218, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Slip Slide Chest": LocData(2000303529, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind Faucet": LocData(2000304214, "Mafia Town Area (HUMT)"), + "Mafia Town - Clock Tower Chest": LocData(2000303481, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Top of Lighthouse": LocData(2000304213, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Mafia Geek Platform": LocData(2000304212, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind HQ Chest": LocData(2000303486, "Mafia Town Area (HUMT)"), + + "Mafia HQ - Hallway Brewing Crate": LocData(2000305387, "Down with the Mafia!", required_hats=[HatType.BREWING]), + "Mafia HQ - Freezer Chest": LocData(2000303241, "Down with the Mafia!"), + "Mafia HQ - Secret Room": LocData(2000304979, "Down with the Mafia!", required_hats=[HatType.ICE]), + "Mafia HQ - Bathroom Stall Chest": LocData(2000303243, "Down with the Mafia!"), + + "Dead Bird Studio - Up the Ladder": LocData(2000304874, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"), + + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + + "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Bottom": LocData(2000305091, "Murder on the Owl Express"), + + "Murder on the Owl Express - Raven Suite Room": LocData(2000305701, "Murder on the Owl Express", + required_hats=[HatType.BREWING]), + + "Murder on the Owl Express - Raven Suite Top": LocData(2000305312, "Murder on the Owl Express"), + "Murder on the Owl Express - Lounge Chest": LocData(2000303963, "Murder on the Owl Express"), + + "Picture Perfect - Behind Badge Seller": LocData(2000304307, "Picture Perfect"), + "Picture Perfect - Hats Buy Building": LocData(2000304530, "Picture Perfect"), + + "Dead Bird Studio Basement - Window Platform": LocData(2000305432, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cardboard Conductor": LocData(2000305059, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Above Conductor Sign": LocData(2000305057, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Logo Wall": LocData(2000305207, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Disco Room": LocData(2000305061, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Small Room": LocData(2000304813, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Vent Pipe": LocData(2000305430, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Tightrope": LocData(2000305058, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cameras": LocData(2000305431, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Locked Room": LocData(2000305819, "Dead Bird Studio Basement", hookshot=True), + + # Subcon Forest + "Contractual Obligations - Cherry Bomb Bone Cage": LocData(2000324761, "Contractual Obligations"), + "Subcon Village - Tree Top Ice Cube": LocData(2000325078, "Subcon Forest Area"), + "Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"), + "Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"), + "Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"), + "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"), + "Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"), + + "Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=1), + + "Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1), + + "Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Forest Treehouse": LocData(2000325467, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage A": LocData(2000324462, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2), + "Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2), + "Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2), + + "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=2), + + "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"), + "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_requirement=2, paintings=1), + + "Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=2), + + "Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=3), + + "Subcon Forest - Dweller Stump": LocData(2000324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Floating Rocks": LocData(2000324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Platforming Tree A": LocData(2000324709, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Platforming Tree B": LocData(2000324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Giant Time Piece": LocData(2000325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(2000325472, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Green and Purple Dweller Rocks": LocData(2000325082, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Shack": LocData(2000324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + hookshot=True, + paintings=3), + + "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), + "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1), + + "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, + paintings=1), + + "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + # Alpine Skyline + "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), + + "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", + required_hats=[HatType.BREWING]), + + "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"), + "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"), + "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"), + + "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(2000336497, "The Birdhouse", + required_hats=[HatType.DWELLER]), + + "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(2000336496, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Hay Bale": LocData(2000335885, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(2000335886, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Outer Edge": LocData(2000335492, "The Birdhouse"), + + "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"), + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"), + "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Top Cake": LocData(2000335418, "The Lava Cake"), + "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), + "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), + "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), + "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"), + "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), + "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), + "Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"), + "Alpine Skyline - The Windmill: House Window": LocData(2000335389, "The Windmill"), + + "The Finale - Frozen Item": LocData(2000304108, "The Finale"), + + "Bon Voyage! - Lamp Post Top": LocData(2000305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "Bon Voyage! - Mafia Cargo Ship": LocData(2000304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Toilet": LocData(2000305109, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Bar": LocData(2000304251, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Dive Board Ledge": LocData(2000304254, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Top Balcony": LocData(2000304255, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room": LocData(2000305253, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room Top": LocData(2000304249, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Laundry Room": LocData(2000304250, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Ship Side": LocData(2000304247, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Silver Ring": LocData(2000305252, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Suitcase": LocData(2000304045, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Under Desk": LocData(2000304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Lamp Post": LocData(2000304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Iceberg Top": LocData(2000304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Post Captain Rescue": LocData(2000304049, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Nyakuza Metro - Main Station Dining Area": LocData(2000304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Nyakuza Metro - Top of Ramen Shop": LocData(2000304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Yellow Overpass Station - Brewing Crate": LocData(2000305413, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.BREWING]), + + "Bluefin Tunnel - Cat Vacuum": LocData(2000305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Pink Paw Station - Cat Vacuum": LocData(2000305110, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Pink Paw Station - Behind Fan": LocData(2000304106, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.TIME_STOP, HatType.DWELLER]), +} + +act_completions = { + "Act Completion (Time Rift - Gallery)": LocData(2000312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), + "Act Completion (Time Rift - The Lab)": LocData(2000312838, "Time Rift - The Lab"), + + "Act Completion (Welcome to Mafia Town)": LocData(2000311771, "Welcome to Mafia Town"), + "Act Completion (Barrel Battle)": LocData(2000311958, "Barrel Battle"), + "Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"), + "Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"), + "Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), + "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", umbrella=True), + "Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"), + "Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"), + "Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"), + "Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"), + + "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", hit_requirement=1), + "Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"), + "Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"), + "Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True), + "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", umbrella=True), + "Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"), + "Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True), + "Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"), + "Act Completion (Time Rift - The Moon)": LocData(2000312785, "Time Rift - The Moon"), + "Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"), + + "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1), + + "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, hit_requirement=1, + paintings=1), + + "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, + paintings=1), + + "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + + "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", + required_hats=[HatType.SPRINT]), + + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True), + "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), + "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"), + "Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"), + + "Act Completion (The Birdhouse)": LocData(2000311428, "The Birdhouse"), + "Act Completion (The Lava Cake)": LocData(2000312509, "The Lava Cake"), + "Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"), + "Act Completion (The Windmill)": LocData(2000312263, "The Windmill"), + "Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True), + + "Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell", + required_hats=[HatType.DWELLER]), + + "Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"), + + "Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), + "Act Completion (Time Rift - Tour)": LocData(2000311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1), + + "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]), + + "Act Completion (Nyakuza Metro Intro)": LocData(2000311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Act Completion (Yellow Overpass Station)": LocData(2000311206, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + hookshot=True), + + "Act Completion (Yellow Overpass Manhole)": LocData(2000311387, "Yellow Overpass Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Green Clean Station)": LocData(2000311207, "Green Clean Station", dlc_flags=HatDLC.dlc2), + + "Act Completion (Green Clean Manhole)": LocData(2000311388, "Green Clean Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE, HatType.DWELLER]), + + "Act Completion (Bluefin Tunnel)": LocData(2000311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Act Completion (Pink Paw Station)": LocData(2000311209, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Act Completion (Pink Paw Manhole)": LocData(2000311389, "Pink Paw Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.ICE, HatType.BREWING]), + + "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), +} + +storybook_pages = { + "Mafia of Cooks - Page: Fish Pile": LocData(2000345091, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Trash Mound": LocData(2000345090, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Beside Red Building": LocData(2000345092, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Behind Shipping Containers": LocData(2000345095, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Top of Boat": LocData(2000345093, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Below Dock": LocData(2000345094, "Time Rift - Mafia of Cooks"), + + "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(2000345449, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(2000345447, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(2000345448, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(2000345450, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(2000345451, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Green Screen": LocData(2000345452, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: In The Corner": LocData(2000345453, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(2000345445, "Time Rift - Dead Bird Studio"), + + "Sleepy Subcon - Page: Behind Entrance Area": LocData(2000345373, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Near Wrecking Ball": LocData(2000345327, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind Crane": LocData(2000345371, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Wrecked Treehouse": LocData(2000345326, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(2000345372, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Rotating Platform": LocData(2000345328, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(2000345329, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Frozen Tree": LocData(2000345330, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Secret Library": LocData(2000345370, "Time Rift - Sleepy Subcon"), + + "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(2000345016, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(2000345012, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(2000345015, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(2000345014, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(2000345013, "Time Rift - Alpine Skyline"), + + "Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + + "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(2000345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - Fragile Box": LocData(2000345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(2000345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Top of Pillar": LocData(2000345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Birdhouse": LocData(2000345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(2000345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: The Finale - Near Entrance": LocData(2000345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Glass Shelf": LocData(2000345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), +} + +shop_locations = { + "Badge Seller - Item 1": LocData(2000301003, "Badge Seller"), + "Badge Seller - Item 2": LocData(2000301004, "Badge Seller"), + "Badge Seller - Item 3": LocData(2000301005, "Badge Seller"), + "Badge Seller - Item 4": LocData(2000301006, "Badge Seller"), + "Badge Seller - Item 5": LocData(2000301007, "Badge Seller"), + "Badge Seller - Item 6": LocData(2000301008, "Badge Seller"), + "Badge Seller - Item 7": LocData(2000301009, "Badge Seller"), + "Badge Seller - Item 8": LocData(2000301010, "Badge Seller"), + "Badge Seller - Item 9": LocData(2000301011, "Badge Seller"), + "Badge Seller - Item 10": LocData(2000301012, "Badge Seller"), + "Mafia Boss Shop Item": LocData(2000301013, "Spaceship"), + + "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2), + + "Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + hookshot=True, required_hats=[HatType.DWELLER]), + + "Main Station Thug A - Item 1": LocData(2000301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 2": LocData(2000301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 3": LocData(2000301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 4": LocData(2000301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 5": LocData(2000301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + + "Main Station Thug B - Item 1": LocData(2000301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 2": LocData(2000301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 3": LocData(2000301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 4": LocData(2000301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 5": LocData(2000301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + + "Main Station Thug C - Item 1": LocData(2000301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 2": LocData(2000301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 3": LocData(2000301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 4": LocData(2000301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 5": LocData(2000301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + + "Yellow Overpass Thug A - Item 1": LocData(2000301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 2": LocData(2000301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 3": LocData(2000301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 4": LocData(2000301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 5": LocData(2000301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + + "Yellow Overpass Thug B - Item 1": LocData(2000301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 2": LocData(2000301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 3": LocData(2000301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 4": LocData(2000301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 5": LocData(2000301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + + "Yellow Overpass Thug C - Item 1": LocData(2000301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 2": LocData(2000301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 3": LocData(2000301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 4": LocData(2000301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 5": LocData(2000301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + + "Green Clean Station Thug A - Item 1": LocData(2000301033, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 2": LocData(2000301034, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 3": LocData(2000301035, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 4": LocData(2000301036, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 5": LocData(2000301037, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + + # This guy requires either the yellow ticket or the Ice Hat + "Green Clean Station Thug B - Item 1": LocData(2000301028, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 2": LocData(2000301029, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 3": LocData(2000301030, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 4": LocData(2000301031, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 5": LocData(2000301032, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + + "Bluefin Tunnel Thug - Item 1": LocData(2000301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 2": LocData(2000301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 3": LocData(2000301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 4": LocData(2000301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 5": LocData(2000301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + + "Pink Paw Station Thug - Item 1": LocData(2000301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 2": LocData(2000301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 3": LocData(2000301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 4": LocData(2000301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 5": LocData(2000301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + +} + +contract_locations = { + "Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area"), +} + +# Don't put any of the locations from peaks here, the rules for their entrances are set already +zipline_unlocks = { + "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - Yellow Band Hills": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Horned Stone": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Chest Reward": "Zipline Unlock - The Birdhouse Path", + + "Alpine Skyline - Mystifying Time Mesa: Zipline": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Ember Summit": "Zipline Unlock - The Lava Cake Path", + + "Alpine Skyline - Goat Outpost Horn": "Zipline Unlock - The Windmill Path", + "Alpine Skyline - Windy Passage": "Zipline Unlock - The Windmill Path", + + "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path", +} + +# act completion rules should be set automatically as these are all event items +zero_jumps_hard = { + "Time Rift - Sewers (Zero Jumps)": LocData(0, "Time Rift - Sewers", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "The Big Parade": LocData(0, "The Big Parade", + umbrella=True, + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Time Rift - Curly Tail Trail (Zero Jumps)": LocData(0, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread", + required_hats=[HatType.ICE], hookshot=True, + hit_requirement=1, dlc_flags=HatDLC.death_wish), + + "The Finale (Zero Jumps)": LocData(0, "The Finale", + required_hats=[HatType.ICE, HatType.DWELLER], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Pink Paw Station (Zero Jumps)": LocData(0, "Pink Paw Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), +} + +zero_jumps_expert = { + "The Birdhouse (Zero Jumps)": LocData(0, "The Birdhouse", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "The Lava Cake (Zero Jumps)": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + + "The Windmill (Zero Jumps)": LocData(0, "The Windmill", + required_hats=[HatType.ICE], + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + "The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + + "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw), +} + +zero_jumps = { + **zero_jumps_hard, + **zero_jumps_expert, + "Welcome to Mafia Town (Zero Jumps)": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), + + "Down with the Mafia! (Zero Jumps)": LocData(0, "Down with the Mafia!", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Cheating the Race (Zero Jumps)": LocData(0, "Cheating the Race", + required_hats=[HatType.TIME_STOP], + dlc_flags=HatDLC.death_wish), + + "The Golden Vault (Zero Jumps)": LocData(0, "The Golden Vault", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio", + required_hats=[HatType.ICE], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Picture Perfect (Zero Jumps)": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Train Rush (Zero Jumps)": LocData(0, "Train Rush", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Contractual Obligations (Zero Jumps)": LocData(0, "Contractual Obligations", + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired", + umbrella=True, + dlc_flags=HatDLC.death_wish), + + # No ice hat/painting required in Expert + "Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom", + hookshot=True, + hit_requirement=1, + required_hats=[HatType.ICE], + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Mail Delivery Service (Zero Jumps)": LocData(0, "Mail Delivery Service", + required_hats=[HatType.SPRINT], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Alpine Skyline (Zero Jumps)": LocData(0, "Time Rift - Alpine Skyline", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Time Rift - The Lab (Zero Jumps)": LocData(0, "Time Rift - The Lab", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Yellow Overpass Station (Zero Jumps)": LocData(0, "Yellow Overpass Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), + + "Green Clean Station (Zero Jumps)": LocData(0, "Green Clean Station", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.dlc2_dw), +} + +# noinspection PyDictDuplicateKeys +snatcher_coins = { + "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", umbrella=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Mafia's Jumps", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Goat Village Top": LocData(0, "The Illness has Speedrun", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "The Windmill", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "Wound-Up Windmill", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2_dw), +} + +event_locs = { + **zero_jumps, + **snatcher_coins, + "HUMT Access": LocData(0, "Heating Up Mafia Town"), + "TOD Access": LocData(0, "Toilet of Doom"), + "YCHE Access": LocData(0, "Your Contract has Expired"), + + "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), + "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), + "Windmill Cleared": LocData(0, "The Windmill", act_event=True), + "Twilight Bell Cleared": LocData(0, "The Twilight Bell", act_event=True), + "Time Piece Cluster": LocData(0, "The Finale", act_event=True), + + # not really an act + "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2, act_event=True), +} + +# DO NOT ALTER THE ORDER OF THIS LIST +death_wishes = { + "Beat the Heat": 2000350000, + "Snatcher's Hit List": 2000350002, + "So You're Back From Outer Space": 2000350004, + "Collect-a-thon": 2000350006, + "Rift Collapse: Mafia of Cooks": 2000350008, + "She Speedran from Outer Space": 2000350010, + "Mafia's Jumps": 2000350012, + "Vault Codes in the Wind": 2000350014, + "Encore! Encore!": 2000350016, + "Snatcher Coins in Mafia Town": 2000350018, + + "Security Breach": 2000350020, + "The Great Big Hootenanny": 2000350022, + "Rift Collapse: Dead Bird Studio": 2000350024, + "10 Seconds until Self-Destruct": 2000350026, + "Killing Two Birds": 2000350028, + "Snatcher Coins in Battle of the Birds": 2000350030, + "Zero Jumps": 2000350032, + + "Speedrun Well": 2000350034, + "Rift Collapse: Sleepy Subcon": 2000350036, + "Boss Rush": 2000350038, + "Quality Time with Snatcher": 2000350040, + "Breaching the Contract": 2000350042, + "Snatcher Coins in Subcon Forest": 2000350044, + + "Bird Sanctuary": 2000350046, + "Rift Collapse: Alpine Skyline": 2000350048, + "Wound-Up Windmill": 2000350050, + "The Illness has Speedrun": 2000350052, + "Snatcher Coins in Alpine Skyline": 2000350054, + "Camera Tourist": 2000350056, + + "The Mustache Gauntlet": 2000350058, + "No More Bad Guys": 2000350060, + + "Seal the Deal": 2000350062, + "Rift Collapse: Deep Sea": 2000350064, + "Cruisin' for a Bruisin'": 2000350066, + + "Community Rift: Rhythm Jump Studio": 2000350068, + "Community Rift: Twilight Travels": 2000350070, + "Community Rift: The Mountain Rift": 2000350072, + "Snatcher Coins in Nyakuza Metro": 2000350074, +} + +location_table = { + **ahit_locations, + **act_completions, + **storybook_pages, + **contract_locations, + **shop_locations, +} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py new file mode 100644 index 0000000000..f3dd2a8c66 --- /dev/null +++ b/worlds/ahit/Options.py @@ -0,0 +1,728 @@ +import typing +from worlds.AutoWorld import World +from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict + + +def adjust_options(world: World): + world.multiworld.HighestChapterCost[world.player].value = max( + world.multiworld.HighestChapterCost[world.player].value, + world.multiworld.LowestChapterCost[world.player].value) + + world.multiworld.LowestChapterCost[world.player].value = min( + world.multiworld.LowestChapterCost[world.player].value, + world.multiworld.HighestChapterCost[world.player].value) + + world.multiworld.FinalChapterMinCost[world.player].value = min( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value) + + world.multiworld.FinalChapterMaxCost[world.player].value = max( + world.multiworld.FinalChapterMaxCost[world.player].value, + world.multiworld.FinalChapterMinCost[world.player].value) + + world.multiworld.BadgeSellerMinItems[world.player].value = min( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.BadgeSellerMaxItems[world.player].value = max( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.NyakuzaThugMinShopItems[world.player].value = min( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.NyakuzaThugMaxShopItems[world.player].value = max( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.DWShuffleCountMin[world.player].value = min( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + world.multiworld.DWShuffleCountMax[world.player].value = max( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + total_tps: int = get_total_time_pieces(world) + if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: + world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.LowestChapterCost[world.player].value > total_tps-5: + world.multiworld.LowestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps: + world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps) + + if world.multiworld.FinalChapterMinCost[world.player].value > total_tps: + world.multiworld.FinalChapterMinCost[world.player].value = min(50, total_tps-5) + + # Don't allow Rush Hour goal if DLC2 content is disabled + if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: + world.multiworld.EndGoal[world.player].value = 1 + + # Don't allow Seal the Deal goal if Death Wish content is disabled + if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw(): + world.multiworld.EndGoal[world.player].value = 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + world.multiworld.DWAutoCompleteBonuses[world.player].value = 0 + + if world.is_dw_only(): + world.multiworld.EndGoal[world.player].value = 3 + world.multiworld.ActRandomizer[world.player].value = 0 + world.multiworld.ShuffleAlpineZiplines[world.player].value = 0 + world.multiworld.ShuffleSubconPaintings[world.player].value = 0 + world.multiworld.ShuffleStorybookPages[world.player].value = 0 + world.multiworld.ShuffleActContracts[world.player].value = 0 + world.multiworld.EnableDLC1[world.player].value = 0 + world.multiworld.LogicDifficulty[world.player].value = -1 + world.multiworld.DWTimePieceRequirement[world.player].value = 0 + + +def get_total_time_pieces(world: World) -> int: + count: int = 40 + if world.is_dlc1(): + count += 6 + + if world.is_dlc2(): + count += 10 + + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + + +class EndGoal(Choice): + """The end goal required to beat the game. + Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location. + + Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7 + will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels. + Requires DLC2 content to be enabled. + + Seal the Deal: Reach and complete the Seal the Deal death wish main objective. + Requires Death Wish content to be enabled.""" + display_name = "End Goal" + option_finale = 1 + option_rush_hour = 2 + option_seal_the_deal = 3 + default = 1 + + +class ActRandomizer(Choice): + """If enabled, shuffle the game's Acts between each other. + Light will cause Time Rifts to only be shuffled amongst each other, + and Blue Time Rifts and Purple Time Rifts to be shuffled separately.""" + display_name = "Shuffle Acts" + option_false = 0 + option_light = 1 + option_insanity = 2 + default = 1 + + +class ActPlando(OptionDict): + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + display_name = "Act Plando" + + +class FinaleShuffle(Toggle): + """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" + display_name = "Finale Shuffle" + default = 0 + + +class LogicDifficulty(Choice): + """Choose the difficulty setting for logic.""" + display_name = "Logic Difficulty" + option_normal = -1 + option_moderate = 0 + option_hard = 1 + option_expert = 2 + default = -1 + + +class CTRLogic(Choice): + """Choose how you want to logically clear Cheating the Race.""" + display_name = "Cheating the Race Logic" + option_time_stop_only = 0 + option_scooter = 1 + option_sprint = 2 + option_nothing = 3 + default = 0 + + +class RandomizeHatOrder(Choice): + """Randomize the order that hats are stitched in. + Time Stop Last will force Time Stop to be the last hat in the sequence.""" + display_name = "Randomize Hat Order" + option_false = 0 + option_true = 1 + option_time_stop_last = 2 + default = 1 + + +class YarnBalancePercent(Range): + """How much (in percentage) of the yarn in the pool that will be progression balanced.""" + display_name = "Yarn Balance Percentage" + default = 20 + range_start = 0 + range_end = 100 + + +class TimePieceBalancePercent(Range): + """How much (in percentage) of time pieces in the pool that will be progression balanced.""" + display_name = "Time Piece Balance Percentage" + default = 35 + range_start = 0 + range_end = 100 + + +class StartWithCompassBadge(Toggle): + """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world + (instead of just Relics). Recommended if you're not familiar with where item locations are.""" + display_name = "Start with Compass Badge" + default = 1 + + +class CompassBadgeMode(Choice): + """closest - Compass Badge points to the closest item regardless of classification + important_only - Compass Badge points to progression/useful items only + important_first - Compass Badge points to progression/useful items first, then it will point to junk items""" + display_name = "Compass Badge Mode" + option_closest = 1 + option_important_only = 2 + option_important_first = 3 + default = 1 + + +class UmbrellaLogic(Toggle): + """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" + display_name = "Umbrella Logic" + default = 0 + + +class ShuffleStorybookPages(Toggle): + """If enabled, each storybook page in the purple Time Rifts is an item check. + The Compass Badge can track these down for you.""" + display_name = "Shuffle Storybook Pages" + default = 1 + + +class ShuffleActContracts(Toggle): + """If enabled, shuffle Snatcher's act contracts into the pool as items""" + display_name = "Shuffle Contracts" + default = 1 + + +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + default = 0 + + +class ShuffleSubconPaintings(Toggle): + """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. + These items are progressive, with the order of Village-Swamp-Courtyard.""" + display_name = "Shuffle Subcon Paintings" + default = 0 + + +class NoPaintingSkips(Toggle): + """If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings.""" + display_name = "No Subcon Fire Wall Skips" + default = 0 + + +class StartingChapter(Choice): + """Determines which chapter you will be guaranteed to be able to enter at the beginning of the game.""" + display_name = "Starting Chapter" + option_1 = 1 + option_2 = 2 + option_3 = 3 + option_4 = 4 + default = 1 + + +class ChapterCostIncrement(Range): + """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" + display_name = "Chapter Cost Increment" + range_start = 1 + range_end = 8 + default = 4 + + +class ChapterCostMinDifference(Range): + """The minimum difference between chapter costs.""" + display_name = "Minimum Chapter Cost Difference" + range_start = 1 + range_end = 8 + default = 4 + + +class LowestChapterCost(Range): + """Value determining the lowest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Lowest Possible Chapter Cost" + range_start = 0 + range_end = 10 + default = 5 + + +class HighestChapterCost(Range): + """Value determining the highest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Highest Possible Chapter Cost" + range_start = 15 + range_end = 45 + default = 25 + + +class FinalChapterMinCost(Range): + """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Minimum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 30 + + +class FinalChapterMaxCost(Range): + """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Maximum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 35 + + +class MaxExtraTimePieces(Range): + """Maximum amount of extra Time Pieces from the DLCs. + Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" + display_name = "Max Extra Time Pieces" + range_start = 0 + range_end = 16 + default = 16 + + +class YarnCostMin(Range): + """The minimum possible yarn needed to stitch a hat.""" + display_name = "Minimum Yarn Cost" + range_start = 1 + range_end = 12 + default = 4 + + +class YarnCostMax(Range): + """The maximum possible yarn needed to stitch a hat.""" + display_name = "Maximum Yarn Cost" + range_start = 1 + range_end = 12 + default = 8 + + +class YarnAvailable(Range): + """How much yarn is available to collect in the item pool.""" + display_name = "Yarn Available" + range_start = 30 + range_end = 80 + default = 50 + + +class MinExtraYarn(Range): + """The minimum amount of extra yarn in the item pool. + There must be at least this much more yarn over the total amount of yarn needed to craft all hats. + For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, + there must be at least 50 yarn in the pool.""" + display_name = "Max Extra Yarn" + range_start = 5 + range_end = 15 + default = 10 + + +class HatItems(Toggle): + """Removes all yarn from the pool and turns the hats into individual items instead.""" + display_name = "Hat Items" + default = 0 + + +class MinPonCost(Range): + """The minimum amount of Pons that any shop item can cost.""" + display_name = "Minimum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 75 + + +class MaxPonCost(Range): + """The maximum amount of Pons that any shop item can cost.""" + display_name = "Maximum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 300 + + +class BadgeSellerMinItems(Range): + """The smallest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Minimum Items" + range_start = 0 + range_end = 10 + default = 4 + + +class BadgeSellerMaxItems(Range): + """The largest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Maximum Items" + range_start = 0 + range_end = 10 + default = 8 + + +class EnableDLC1(Toggle): + """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 6" + default = 0 + + +class Tasksanity(Toggle): + """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled.""" + display_name = "Tasksanity" + default = 0 + + +class TasksanityTaskStep(Range): + """How many tasks the player must complete in Tasksanity to send a check.""" + display_name = "Tasksanity Task Step" + range_start = 1 + range_end = 3 + default = 1 + + +class TasksanityCheckCount(Range): + """How many Tasksanity checks there will be in total.""" + display_name = "Tasksanity Check Count" + range_start = 5 + range_end = 30 + default = 18 + + +class ExcludeTour(Toggle): + """Removes the Tour time rift from the game. This option is recommended if you don't want to deal with + important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages + when your goal is Time's End.""" + display_name = "Exclude Tour Time Rift" + default = 0 + + +class ShipShapeCustomTaskGoal(Range): + """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + display_name = "Ship Shape Custom Task Goal" + range_start = 1 + range_end = 30 + default = 18 + + +class EnableDLC2(Toggle): + """Shuffle content from Nyakuza Metro (Chapter 7) into the game. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 7" + default = 0 + + +class MetroMinPonCost(Range): + """The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Minimum Pon Cost" + range_start = 10 + range_end = 800 + default = 50 + + +class MetroMaxPonCost(Range): + """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Maximum Pon Cost" + range_start = 10 + range_end = 800 + default = 200 + + +class NyakuzaThugMinShopItems(Range): + """The smallest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Minimum Shop Items" + range_start = 0 + range_end = 5 + default = 2 + + +class NyakuzaThugMaxShopItems(Range): + """The largest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Maximum Shop Items" + range_start = 0 + range_end = 5 + default = 4 + + +class BaseballBat(Toggle): + """Replace the Umbrella with the baseball bat from Nyakuza Metro. + DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" + display_name = "Baseball Bat" + default = 0 + + +class EnableDeathWish(Toggle): + """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Enable Death Wish" + default = 0 + + +class DeathWishOnly(Toggle): + """An alternative gameplay mode that allows you to exclusively play Death Wish in a seed. + This has the following effects: + - Death Wish is instantly unlocked from the start + - All hats and other progression items are instantly given to you + - Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start + - All chapters and their levels are unlocked, act shuffle is forced off + - Any checks other than Death Wish contracts are completely removed + - All Pons in the item pool are replaced with Health Pons or random cosmetics + - The EndGoal option is forced to complete Seal the Deal""" + display_name = "Death Wish Only" + default = 0 + + +class DWShuffle(Toggle): + """An alternative mode for Death Wish where each contract is unlocked one by one, in a random order. + Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence. + If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence. + Disabling candles is highly recommended.""" + display_name = "Death Wish Shuffle" + default = 0 + + +class DWShuffleCountMin(Range): + """The minimum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Minimum Count" + range_start = 5 + range_end = 38 + default = 18 + + +class DWShuffleCountMax(Range): + """The maximum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Maximum Count" + range_start = 5 + range_end = 38 + default = 25 + + +class DWEnableBonus(Toggle): + """In Death Wish, allow the full completion of contracts to reward items. + WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! + ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! + Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" + display_name = "Shuffle Death Wish Full Completions" + default = 0 + + +class DWAutoCompleteBonuses(Toggle): + """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish. + This option will have no effect if bonus checks (DWEnableBonus) are turned on.""" + display_name = "Auto Complete Bonus Stamps" + default = 1 + + +class DWExcludeAnnoyingContracts(Toggle): + """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear. + Excluded Death Wishes are automatically completed as soon as they are unlocked. + This option currently excludes the following contracts: + - Vault Codes in the Wind + - Boss Rush + - Camera Tourist + - The Mustache Gauntlet + - Rift Collapse: Deep Sea + - Cruisin' for a Bruisin' + - Seal the Deal (non-excluded if goal, but the checks are still excluded)""" + display_name = "Exclude Annoying Death Wish Contracts" + default = 1 + + +class DWExcludeAnnoyingBonuses(Toggle): + """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool. + Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective. + This option currently excludes the following bonuses: + - So You're Back From Outer Space + - Encore! Encore! + - Snatcher's Hit List + - 10 Seconds until Self-Destruct + - Killing Two Birds + - Zero Jumps + - Bird Sanctuary + - Wound-Up Windmill + - Seal the Deal""" + display_name = "Exclude Annoying Death Wish Full Completions" + default = 1 + + +class DWExcludeCandles(Toggle): + """If enabled, exclude all candle Death Wishes.""" + display_name = "Exclude Candle Death Wishes" + default = 1 + + +class DWTimePieceRequirement(Range): + """How many Time Pieces that will be required to unlock Death Wish.""" + display_name = "Death Wish Time Piece Requirement" + range_start = 0 + range_end = 35 + default = 15 + + +class TrapChance(Range): + """The chance for any junk item in the pool to be replaced by a trap.""" + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 0 + + +class BabyTrapWeight(Range): + """The weight of Baby Traps in the trap pool. + Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance.""" + display_name = "Baby Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class LaserTrapWeight(Range): + """The weight of Laser Traps in the trap pool. + Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location.""" + display_name = "Laser Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class ParadeTrapWeight(Range): + """The weight of Parade Traps in the trap pool. + Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement.""" + display_name = "Parade Trap Weight" + range_start = 0 + range_end = 100 + default = 20 + + +ahit_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ActPlando": ActPlando, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "FinaleShuffle": FinaleShuffle, + "LogicDifficulty": LogicDifficulty, + "YarnBalancePercent": YarnBalancePercent, + "TimePieceBalancePercent": TimePieceBalancePercent, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, + "StartingChapter": StartingChapter, + "CTRLogic": CTRLogic, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ExcludeTour": ExcludeTour, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DWShuffleCountMin": DWShuffleCountMin, + "DWShuffleCountMax": DWShuffleCountMax, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts, + "DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses, + "DWExcludeCandles": DWExcludeCandles, + "DWTimePieceRequirement": DWTimePieceRequirement, + + "EnableDLC2": EnableDLC2, + "BaseballBat": BaseballBat, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, + "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + + "LowestChapterCost": LowestChapterCost, + "HighestChapterCost": HighestChapterCost, + "ChapterCostIncrement": ChapterCostIncrement, + "ChapterCostMinDifference": ChapterCostMinDifference, + "MaxExtraTimePieces": MaxExtraTimePieces, + + "FinalChapterMinCost": FinalChapterMinCost, + "FinalChapterMaxCost": FinalChapterMaxCost, + + "YarnCostMin": YarnCostMin, + "YarnCostMax": YarnCostMax, + "YarnAvailable": YarnAvailable, + "MinExtraYarn": MinExtraYarn, + "HatItems": HatItems, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + "BadgeSellerMinItems": BadgeSellerMinItems, + "BadgeSellerMaxItems": BadgeSellerMaxItems, + + "TrapChance": TrapChance, + "BabyTrapWeight": BabyTrapWeight, + "LaserTrapWeight": LaserTrapWeight, + "ParadeTrapWeight": ParadeTrapWeight, + + "death_link": DeathLink, +} + +slot_data_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "LogicDifficulty": LogicDifficulty, + "CTRLogic": CTRLogic, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, + "HatItems": HatItems, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, + + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWTimePieceRequirement": DWTimePieceRequirement, + + "EnableDLC2": EnableDLC2, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "BaseballBat": BaseballBat, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + + "death_link": DeathLink, +} diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py new file mode 100644 index 0000000000..807f1ee77f --- /dev/null +++ b/worlds/ahit/Regions.py @@ -0,0 +1,900 @@ +from worlds.AutoWorld import World +from BaseClasses import Region, Entrance, ItemClassification, Location +from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem +from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ + shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard +import typing +from .Rules import set_rift_rules, get_difficulty + + +# ChapterIndex: region +chapter_regions = { + ChapterIndex.SPACESHIP: "Spaceship", + ChapterIndex.MAFIA: "Mafia Town", + ChapterIndex.BIRDS: "Battle of the Birds", + ChapterIndex.SUBCON: "Subcon Forest", + ChapterIndex.ALPINE: "Alpine Skyline", + ChapterIndex.FINALE: "Time's End", + ChapterIndex.CRUISE: "The Arctic Cruise", + ChapterIndex.METRO: "Nyakuza Metro", +} + +# entrance: region +act_entrances = { + "Welcome to Mafia Town": "Mafia Town - Act 1", + "Barrel Battle": "Mafia Town - Act 2", + "She Came from Outer Space": "Mafia Town - Act 3", + "Down with the Mafia!": "Mafia Town - Act 4", + "Cheating the Race": "Mafia Town - Act 5", + "Heating Up Mafia Town": "Mafia Town - Act 6", + "The Golden Vault": "Mafia Town - Act 7", + + "Dead Bird Studio": "Battle of the Birds - Act 1", + "Murder on the Owl Express": "Battle of the Birds - Act 2", + "Picture Perfect": "Battle of the Birds - Act 3", + "Train Rush": "Battle of the Birds - Act 4", + "The Big Parade": "Battle of the Birds - Act 5", + "Award Ceremony": "Battle of the Birds - Finale A", + "Dead Bird Studio Basement": "Battle of the Birds - Finale B", + + "Contractual Obligations": "Subcon Forest - Act 1", + "The Subcon Well": "Subcon Forest - Act 2", + "Toilet of Doom": "Subcon Forest - Act 3", + "Queen Vanessa's Manor": "Subcon Forest - Act 4", + "Mail Delivery Service": "Subcon Forest - Act 5", + "Your Contract has Expired": "Subcon Forest - Finale", + + "Alpine Free Roam": "Alpine Skyline - Free Roam", + "The Illness has Spread": "Alpine Skyline - Finale", + + "The Finale": "Time's End - Act 1", + + "Bon Voyage!": "The Arctic Cruise - Act 1", + "Ship Shape": "The Arctic Cruise - Act 2", + "Rock the Boat": "The Arctic Cruise - Finale", + + "Nyakuza Free Roam": "Nyakuza Metro - Free Roam", + "Rush Hour": "Nyakuza Metro - Finale", +} + +act_chapters = { + "Time Rift - Gallery": "Spaceship", + "Time Rift - The Lab": "Spaceship", + + "Welcome to Mafia Town": "Mafia Town", + "Barrel Battle": "Mafia Town", + "She Came from Outer Space": "Mafia Town", + "Down with the Mafia!": "Mafia Town", + "Cheating the Race": "Mafia Town", + "Heating Up Mafia Town": "Mafia Town", + "The Golden Vault": "Mafia Town", + "Time Rift - Mafia of Cooks": "Mafia Town", + "Time Rift - Sewers": "Mafia Town", + "Time Rift - Bazaar": "Mafia Town", + + "Dead Bird Studio": "Battle of the Birds", + "Murder on the Owl Express": "Battle of the Birds", + "Picture Perfect": "Battle of the Birds", + "Train Rush": "Battle of the Birds", + "The Big Parade": "Battle of the Birds", + "Award Ceremony": "Battle of the Birds", + "Dead Bird Studio Basement": "Battle of the Birds", + "Time Rift - Dead Bird Studio": "Battle of the Birds", + "Time Rift - The Owl Express": "Battle of the Birds", + "Time Rift - The Moon": "Battle of the Birds", + + "Contractual Obligations": "Subcon Forest", + "The Subcon Well": "Subcon Forest", + "Toilet of Doom": "Subcon Forest", + "Queen Vanessa's Manor": "Subcon Forest", + "Mail Delivery Service": "Subcon Forest", + "Your Contract has Expired": "Subcon Forest", + "Time Rift - Sleepy Subcon": "Subcon Forest", + "Time Rift - Pipe": "Subcon Forest", + "Time Rift - Village": "Subcon Forest", + + "Alpine Free Roam": "Alpine Skyline", + "The Illness has Spread": "Alpine Skyline", + "Time Rift - Alpine Skyline": "Alpine Skyline", + "Time Rift - The Twilight Bell": "Alpine Skyline", + "Time Rift - Curly Tail Trail": "Alpine Skyline", + + "The Finale": "Time's End", + "Time Rift - Tour": "Time's End", + + "Bon Voyage!": "The Arctic Cruise", + "Ship Shape": "The Arctic Cruise", + "Rock the Boat": "The Arctic Cruise", + "Time Rift - Balcony": "The Arctic Cruise", + "Time Rift - Deep Sea": "The Arctic Cruise", + + "Nyakuza Free Roam": "Nyakuza Metro", + "Rush Hour": "Nyakuza Metro", + "Time Rift - Rumbi Factory": "Nyakuza Metro", +} + +# region: list[Region] +rift_access_regions = { + "Time Rift - Gallery": ["Spaceship"], + "Time Rift - The Lab": ["Spaceship"], + + "Time Rift - Sewers": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Bazaar": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Mafia of Cooks": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "The Golden Vault"], + + "Time Rift - The Owl Express": ["Murder on the Owl Express"], + "Time Rift - The Moon": ["Picture Perfect", "The Big Parade"], + "Time Rift - Dead Bird Studio": ["Dead Bird Studio", "Dead Bird Studio Basement"], + + "Time Rift - Pipe": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Village": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Sleepy Subcon": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - The Twilight Bell": ["Alpine Free Roam"], + "Time Rift - Curly Tail Trail": ["Alpine Free Roam"], + "Time Rift - Alpine Skyline": ["Alpine Free Roam", "The Illness has Spread"], + + "Time Rift - Tour": ["Time's End"], + + "Time Rift - Balcony": ["Cruise Ship"], + "Time Rift - Deep Sea": ["Bon Voyage!"], + + "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"], +} + +# Time piece identifiers to be used in act shuffle +chapter_act_info = { + "Time Rift - Gallery": "Spaceship_WaterRift_Gallery", + "Time Rift - The Lab": "Spaceship_WaterRift_MailRoom", + + "Welcome to Mafia Town": "chapter1_tutorial", + "Barrel Battle": "chapter1_barrelboss", + "She Came from Outer Space": "chapter1_cannon_repair", + "Down with the Mafia!": "chapter1_boss", + "Cheating the Race": "harbor_impossible_race", + "Heating Up Mafia Town": "mafiatown_lava", + "The Golden Vault": "mafiatown_goldenvault", + "Time Rift - Mafia of Cooks": "TimeRift_Cave_Mafia", + "Time Rift - Sewers": "TimeRift_Water_Mafia_Easy", + "Time Rift - Bazaar": "TimeRift_Water_Mafia_Hard", + + "Dead Bird Studio": "DeadBirdStudio", + "Murder on the Owl Express": "chapter3_murder", + "Picture Perfect": "moon_camerasnap", + "Train Rush": "trainwreck_selfdestruct", + "The Big Parade": "moon_parade", + "Award Ceremony": "award_ceremony", + "Dead Bird Studio Basement": "chapter3_secret_finale", + "Time Rift - Dead Bird Studio": "TimeRift_Cave_BirdBasement", + "Time Rift - The Owl Express": "TimeRift_Water_TWreck_Panels", + "Time Rift - The Moon": "TimeRift_Water_TWreck_Parade", + + "Contractual Obligations": "subcon_village_icewall", + "The Subcon Well": "subcon_cave", + "Toilet of Doom": "chapter2_toiletboss", + "Queen Vanessa's Manor": "vanessa_manor_attic", + "Mail Delivery Service": "subcon_maildelivery", + "Your Contract has Expired": "snatcher_boss", + "Time Rift - Sleepy Subcon": "TimeRift_Cave_Raccoon", + "Time Rift - Pipe": "TimeRift_Water_Subcon_Hookshot", + "Time Rift - Village": "TimeRift_Water_Subcon_Dwellers", + + "Alpine Free Roam": "AlpineFreeRoam", # not an actual Time Piece + "The Illness has Spread": "AlpineSkyline_Finale", + "Time Rift - Alpine Skyline": "TimeRift_Cave_Alps", + "Time Rift - The Twilight Bell": "TimeRift_Water_Alp_Goats", + "Time Rift - Curly Tail Trail": "TimeRift_Water_AlpineSkyline_Cats", + + "The Finale": "TheFinale_FinalBoss", + "Time Rift - Tour": "TimeRift_Cave_Tour", + + "Bon Voyage!": "Cruise_Boarding", + "Ship Shape": "Cruise_Working", + "Rock the Boat": "Cruise_Sinking", + "Time Rift - Balcony": "Cruise_WaterRift_Slide", + "Time Rift - Deep Sea": "Cruise_CaveRift_Aquarium", + + "Nyakuza Free Roam": "MetroFreeRoam", # not an actual Time Piece + "Rush Hour": "Metro_Escape", + "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory" +} + +# Guarantee that the first level a player can access is a location dense area beatable with no items +guaranteed_first_acts = [ + "Welcome to Mafia Town", + "Barrel Battle", + "She Came from Outer Space", + "Down with the Mafia!", + "Heating Up Mafia Town", # Removed in umbrella logic + "The Golden Vault", + + "Contractual Obligations", # Removed in painting logic + "Queen Vanessa's Manor", # Removed in umbrella/painting logic +] + +purple_time_rifts = [ + "Time Rift - Mafia of Cooks", + "Time Rift - Dead Bird Studio", + "Time Rift - Sleepy Subcon", + "Time Rift - Alpine Skyline", + "Time Rift - Deep Sea", + "Time Rift - Tour", + "Time Rift - Rumbi Factory", +] + +chapter_finales = [ + "Dead Bird Studio Basement", + "Your Contract has Expired", + "The Illness has Spread", + "Rock the Boat", + "Rush Hour", +] + +# Acts blacklisted in act shuffle +# entrance: region +blacklisted_acts = { + "Battle of the Birds - Finale A": "Award Ceremony", +} + +# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. +blacklisted_combos = { + "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!", + "Contractual Obligations"], + + "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"], + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], +} + + +def create_regions(world: World): + w = world + mw = world.multiworld + p = world.player + + # ------------------------------------------- HUB -------------------------------------------------- # + menu = create_region(w, "Menu") + spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + + # we only need the menu and the spaceship regions + if world.is_dw_only(): + return + + create_rift_connections(w, create_region(w, "Time Rift - Gallery")) + create_rift_connections(w, create_region(w, "Time Rift - The Lab")) + + # ------------------------------------------- MAFIA TOWN ------------------------------------------- # + mafia_town = create_region_and_connect(w, "Mafia Town", "Telescope -> Mafia Town", spaceship) + mt_act1 = create_region_and_connect(w, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town) + mt_act2 = create_region_and_connect(w, "Barrel Battle", "Mafia Town - Act 2", mafia_town) + mt_act3 = create_region_and_connect(w, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town) + mt_act4 = create_region_and_connect(w, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town) + mt_act6 = create_region_and_connect(w, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town) + mt_act5 = create_region_and_connect(w, "Cheating the Race", "Mafia Town - Act 5", mafia_town) + mt_act7 = create_region_and_connect(w, "The Golden Vault", "Mafia Town - Act 7", mafia_town) + + # ------------------------------------------- BOTB ------------------------------------------------- # + botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) + dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) + create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) + pp = create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) + tr = create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) + create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb) + create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb) + basement = create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) + create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio")) + create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) + create_rift_connections(w, create_region(w, "Time Rift - The Moon")) + + # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert + ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) + post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) + if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): + connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + + # ------------------------------------------- SUBCON FOREST --------------------------------------- # + subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) + sf_act1 = create_region_and_connect(w, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest) + sf_act2 = create_region_and_connect(w, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest) + sf_act3 = create_region_and_connect(w, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) + sf_act4 = create_region_and_connect(w, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) + sf_act5 = create_region_and_connect(w, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) + create_region_and_connect(w, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) + + # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ # + alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) + alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) + alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) + + # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness + alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", + alpine_area) + + create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) + create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) + create_region_and_connect(w, "The Windmill", "-> The Windmill", alpine_area) + create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) + + illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) + connect_regions(illness, alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)", p) + create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) + create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) + create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) + + # ------------------------------------------- OTHER -------------------------------------------------- # + mt_area: Region = create_region(w, "Mafia Town Area") + mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)") + connect_regions(mt_area, mt_area_humt, "MT Area -> MT Area (HUMT)", p) + connect_regions(mt_act1, mt_area, "Mafia Town Entrance WTMT", p) + connect_regions(mt_act2, mt_area, "Mafia Town Entrance BB", p) + connect_regions(mt_act3, mt_area, "Mafia Town Entrance SCFOS", p) + connect_regions(mt_act4, mt_area, "Mafia Town Entrance DWTM", p) + connect_regions(mt_act5, mt_area, "Mafia Town Entrance CTR", p) + connect_regions(mt_act6, mt_area_humt, "Mafia Town Entrance HUMT", p) + connect_regions(mt_act7, mt_area, "Mafia Town Entrance TGV", p) + + create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks")) + create_rift_connections(w, create_region(w, "Time Rift - Sewers")) + create_rift_connections(w, create_region(w, "Time Rift - Bazaar")) + + sf_area: Region = create_region(w, "Subcon Forest Area") + connect_regions(sf_act1, sf_area, "Subcon Forest Entrance CO", p) + connect_regions(sf_act2, sf_area, "Subcon Forest Entrance SW", p) + connect_regions(sf_act3, sf_area, "Subcon Forest Entrance TOD", p) + connect_regions(sf_act4, sf_area, "Subcon Forest Entrance QVM", p) + connect_regions(sf_act5, sf_area, "Subcon Forest Entrance MDS", p) + + create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon")) + create_rift_connections(w, create_region(w, "Time Rift - Pipe")) + create_rift_connections(w, create_region(w, "Time Rift - Village")) + + badge_seller = create_badge_seller(w) + connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p) + connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p) + connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p) + connect_regions(dbs, badge_seller, "DBS -> Badge Seller", p) + connect_regions(pp, badge_seller, "PP -> Badge Seller", p) + connect_regions(tr, badge_seller, "TR -> Badge Seller", p) + connect_regions(alpine_area_tihs, badge_seller, "ASA -> Badge Seller", p) + + times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) + create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) + + # ------------------------------------------- DLC1 ------------------------------------------------- # + if w.is_dlc1(): + arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship) + cruise_ship = create_region(w, "Cruise Ship") + + ac_act1 = create_region_and_connect(w, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise) + ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) + ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) + + connect_regions(ac_act1, cruise_ship, "Cruise Ship Entrance BV", p) + connect_regions(ac_act2, cruise_ship, "Cruise Ship Entrance SS", p) + connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) + create_rift_connections(w, create_region(w, "Time Rift - Balcony")) + create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) + + if mw.ExcludeTour[world.player].value == 0: + create_rift_connections(w, create_region(w, "Time Rift - Tour")) + + if mw.Tasksanity[p].value > 0: + create_tasksanity_locations(w) + + connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) + + if w.is_dlc2(): + nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) + metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro) + create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro) + + yellow = create_region_and_connect(w, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam) + green = create_region_and_connect(w, "Green Clean Station", "-> Green Clean Station", metro_freeroam) + pink = create_region_and_connect(w, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam) + create_region_and_connect(w, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole + + create_region_and_connect(w, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow) + create_region_and_connect(w, "Green Clean Manhole", "-> Green Clean Manhole", green) + create_region_and_connect(w, "Pink Paw Manhole", "-> Pink Paw Manhole", pink) + + create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory")) + create_thug_shops(w) + + +def create_rift_connections(world: World, region: Region): + i = 1 + for name in rift_access_regions[region.name]: + act_region = world.multiworld.get_region(name, world.player) + entrance_name = f"{region.name} Portal - Entrance {i}" + connect_regions(act_region, region, entrance_name, world.player) + i += 1 + + # fix for some weird keyerror from tests + if region.name == "Time Rift - Rumbi Factory": + for entrance in region.entrances: + world.multiworld.get_entrance(entrance.name, world.player) + + +def create_tasksanity_locations(world: World): + ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) + id_start: int = TASKSANITY_START_ID + for i in range(world.multiworld.TasksanityCheckCount[world.player].value): + location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) + ship_shape.locations.append(location) + + +def is_valid_plando(world: World, region: str) -> bool: + if region in blacklisted_acts.values(): + return False + + if region not in world.multiworld.ActPlando[world.player].keys(): + return False + + act = world.multiworld.ActPlando[world.player].get(region) + if act in blacklisted_acts.values(): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ + and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) + + if is_first_act: + if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + return False + + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): + return False + + if act not in guaranteed_first_acts: + return False + + # Don't allow straight up impossible mappings + if region == "The Illness has Spread" and act == "Alpine Free Roam": + return False + + if region == "Rush Hour" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in + world.multiworld.get_regions(world.player)) + + +def randomize_act_entrances(world: World): + region_list: typing.List[Region] = get_act_regions(world) + world.random.shuffle(region_list) + + separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) + + for region in region_list.copy(): + if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ + and "Time Rift" not in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if "Time Rift" in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if region.name in chapter_finales: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if region.name in world.multiworld.ActPlando[world.player].keys(): + if is_valid_plando(world, region.name): + region_list.remove(region) + region_list.append(region) + else: + print("Disallowing act plando for", + world.multiworld.player_name[world.player], + "-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name)) + + # Reverse the list, so we can do what we want to do first + region_list.reverse() + + shuffled_list: typing.List[Region] = [] + mapped_list: typing.List[Region] = [] + rift_dict: typing.Dict[str, Region] = {} + first_chapter: Region = get_first_chapter_region(world) + has_guaranteed: bool = False + + i: int = 0 + while i < len(region_list): + region = region_list[i] + i += 1 + + # Get the first accessible act, so we can map that to something first + if not has_guaranteed: + if act_chapters[region.name] != first_chapter.name: + continue + + if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \ + and "Free Roam" not in act_entrances[region.name]: + continue + + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + has_guaranteed = True + + i = 0 + + # Already mapped to something else + if region in mapped_list: + continue + + mapped_list.append(region) + + # Look for candidates to map this act to + candidate_list: typing.List[Region] = [] + for candidate in region_list: + # We're mapping something to the first act, make sure it is valid + if not has_guaranteed: + if candidate.name not in guaranteed_first_acts: + continue + + if candidate.name in world.multiworld.ActPlando[world.player].values(): + continue + + # Not completable without Umbrella + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): + continue + + # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0 \ + and "Subcon Forest" in act_entrances[candidate.name]: + continue + + candidate_list.append(candidate) + has_guaranteed = True + break + + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + candidate_list.clear() + candidate_list.append( + world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player)) + break + + # Already mapped onto something else + if candidate in shuffled_list: + continue + + if separate_rifts: + # Don't map Time Rifts to normal acts + if "Time Rift" in region.name and "Time Rift" not in candidate.name: + continue + + # Don't map normal acts to Time Rifts + if "Time Rift" not in region.name and "Time Rift" in candidate.name: + continue + + # Separate purple rifts + if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \ + or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: + continue + + if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: + continue + + # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled + if world.multiworld.ShuffleActContracts[world.player].value == 0: + if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ + and candidate.name == "Contractual Obligations": + continue + + if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: + if candidate.name not in chapter_finales: + continue + + if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: + continue + + candidate_list.append(candidate) + + candidate: Region + if len(candidate_list) > 0: + candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] + else: + # plando can still break certain rules, so acts may not always end up shuffled. + for c in region_list: + if c not in shuffled_list: + candidate = c + break + + shuffled_list.append(candidate) + # print(region, candidate) + + # Vanilla + if candidate.name == region.name: + if region.name in rift_access_regions.keys(): + rift_dict.setdefault(region.name, candidate) + + update_chapter_act_info(world, region, candidate) + continue + + if region.name in rift_access_regions.keys(): + connect_time_rift(world, region, candidate) + rift_dict.setdefault(region.name, candidate) + else: + if candidate.name in rift_access_regions.keys(): + for e in candidate.entrances.copy(): + e.parent_region.exits.remove(e) + e.connected_region.entrances.remove(e) + + entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) + reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) + + update_chapter_act_info(world, region, candidate) + + for name in blacklisted_acts.values(): + if not is_act_blacklisted(world, name): + continue + + region: Region = world.multiworld.get_region(name, world.player) + update_chapter_act_info(world, region, region) + + set_rift_rules(world, rift_dict) + + +def connect_time_rift(world: World, time_rift: Region, exit_region: Region): + count: int = len(rift_access_regions[time_rift.name]) + i: int = 1 + while i <= count: + name = f"{time_rift.name} Portal - Entrance {i}" + entrance: Entrance = world.multiworld.get_entrance(name, world.player) + reconnect_regions(entrance, entrance.parent_region, exit_region) + i += 1 + + +def get_act_regions(world: World) -> typing.List[Region]: + act_list: typing.List[Region] = [] + for region in world.multiworld.get_regions(world.player): + if region.name in chapter_act_info.keys(): + if not is_act_blacklisted(world, region.name): + act_list.append(region) + + return act_list + + +def is_act_blacklisted(world: World, name: str) -> bool: + plando: bool = name in world.multiworld.ActPlando[world.player].keys() \ + or name in world.multiworld.ActPlando[world.player].values() + + if name == "The Finale": + return not plando and world.multiworld.EndGoal[world.player].value == 1 + + if name == "Rush Hour": + return not plando and world.multiworld.EndGoal[world.player].value == 2 + + if name == "Time Rift - Tour": + return world.multiworld.ExcludeTour[world.player].value > 0 + + return name in blacklisted_acts.values() + + +def create_region(world: World, name: str) -> Region: + reg = Region(name, world.player, world.multiworld) + + for (key, data) in location_table.items(): + if world.is_dw_only(): + break + + if data.nyakuza_thug != "": + continue + + if data.region == name: + if key in storybook_pages.keys() \ + and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + continue + + location = HatInTimeLocation(world.player, key, data.id, reg) + reg.locations.append(location) + if location.name in shop_locations: + world.shop_locs.append(location.name) + + world.multiworld.regions.append(reg) + return reg + + +def create_badge_seller(world: World) -> Region: + badge_seller = Region("Badge Seller", world.player, world.multiworld) + world.multiworld.regions.append(badge_seller) + count: int = 0 + max_items: int = 0 + + if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: + max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + if max_items <= 0: + world.set_badge_seller_count(0) + return badge_seller + + for (key, data) in shop_locations.items(): + if "Badge Seller" not in key: + continue + + location = HatInTimeLocation(world.player, key, data.id, badge_seller) + badge_seller.locations.append(location) + world.shop_locs.append(location.name) + + count += 1 + if count >= max_items: + break + + world.set_badge_seller_count(max_items) + return badge_seller + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance + + +# Takes an entrance, removes its old connections, and reconnects it between the two regions specified. +def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region): + if entrance in entrance.connected_region.entrances: + entrance.connected_region.entrances.remove(entrance) + + if entrance in entrance.parent_region.exits: + entrance.parent_region.exits.remove(entrance) + + if entrance in start_region.exits: + start_region.exits.remove(entrance) + + if entrance in exit_region.entrances: + exit_region.entrances.remove(entrance) + + entrance.parent_region = start_region + start_region.exits.append(entrance) + entrance.connect(exit_region) + + +def create_region_and_connect(world: World, + name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region: + + reg: Region = create_region(world, name) + entrance_region: Region + exit_region: Region + + if is_exit: + entrance_region = connected_region + exit_region = reg + else: + entrance_region = reg + exit_region = connected_region + + connect_regions(entrance_region, exit_region, entrancename, world.player) + return reg + + +def get_first_chapter_region(world: World) -> Region: + start_chapter: ChapterIndex = world.multiworld.StartingChapter[world.player] + return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) + + +def get_act_original_chapter(world: World, act_name: str) -> Region: + return world.multiworld.get_region(act_chapters[act_name], world.player) + + +# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game +def update_chapter_act_info(world: World, original_region: Region, new_region: Region): + original_act_info = chapter_act_info[original_region.name] + new_act_info = chapter_act_info[new_region.name] + world.act_connections[original_act_info] = new_act_info + + +def get_shuffled_region(self, region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in self.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + return name + + +def create_thug_shops(world: World): + min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value + + max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value + count: int = -1 + step: int = 0 + old_name: str = "" + thug_items = world.get_nyakuza_thug_items() + + for key, data in shop_locations.items(): + if data.nyakuza_thug == "": + continue + + if old_name != "" and old_name == data.nyakuza_thug: + continue + + try: + if thug_items[data.nyakuza_thug] <= 0: + continue + except KeyError: + pass + + if count == -1: + count = world.random.randint(min_items, max_items) + thug_items.setdefault(data.nyakuza_thug, count) + if count <= 0: + continue + + if count >= 1: + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + + step += 1 + if step >= count: + old_name = data.nyakuza_thug + step = 0 + count = -1 + + world.set_nyakuza_thug_items(thug_items) + + +def create_events(world: World) -> int: + count: int = 0 + + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + item_name: str = name + if world.is_dw(): + if name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + elif name in zero_jumps: + if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: + continue + + event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world) + event.show_in_spoiler = False + count += 1 + + return count + + +def create_event(name: str, item_name: str, region: Region, world: World) -> Location: + event = HatInTimeLocation(world.player, name, None, region) + region.locations.append(event) + event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) + return event diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py new file mode 100644 index 0000000000..7eb09bedfc --- /dev/null +++ b/worlds/ahit/Rules.py @@ -0,0 +1,944 @@ +from worlds.AutoWorld import World, CollectionState +from worlds.generic.Rules import add_rule, set_rule +from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ + shop_locations, event_locs, snatcher_coins +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC +from BaseClasses import Location, Entrance, Region +import typing + + +act_connections = { + "Mafia Town - Act 2": ["Mafia Town - Act 1"], + "Mafia Town - Act 3": ["Mafia Town - Act 1"], + "Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"], + "Mafia Town - Act 6": ["Mafia Town - Act 4"], + "Mafia Town - Act 7": ["Mafia Town - Act 4"], + "Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"], + + "Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"], + "Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"], + + "Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2", + "Subcon Forest - Act 3", "Subcon Forest - Act 4", + "Subcon Forest - Act 5"], + + "The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"], + "The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"], +} + + +def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: + if world.multiworld.HatItems[world.player].value > 0: + return state.has(hat_type_to_item[hat], world.player) + + return state.count("Yarn", world.player) >= get_hat_cost(world, hat) + + +def get_hat_cost(world: World, hat: HatType) -> int: + cost: int = 0 + costs = world.get_hat_yarn_costs() + for h in world.get_hat_craft_order(): + cost += costs[h] + if h == hat: + break + + return cost + + +def can_sdj(state: CollectionState, world: World): + return can_use_hat(state, world, HatType.SPRINT) + + +def painting_logic(world: World) -> bool: + return world.multiworld.ShuffleSubconPaintings[world.player].value > 0 + + +# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert +def get_difficulty(world: World) -> Difficulty: + return Difficulty(world.multiworld.LogicDifficulty[world.player].value) + + +def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: + if not painting_logic(world): + return True + + if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip: + # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena + if get_difficulty(world) >= Difficulty.MODERATE: + return True + + return state.count("Progressive Painting Unlock", world.player) >= count + + +def zipline_logic(world: World) -> bool: + return world.multiworld.ShuffleAlpineZiplines[world.player].value > 0 + + +def can_use_hookshot(state: CollectionState, world: World): + return state.has("Hookshot Badge", world.player) + + +def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): + if world.multiworld.UmbrellaLogic[world.player].value == 0: + return True + + return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) + + +def can_surf(state: CollectionState, world: World): + return state.has("No Bonk Badge", world.player) + + +def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: + return state.has_group(relic, world.player, len(world.item_name_groups[relic])) + + +def get_relic_count(state: CollectionState, world: World, relic: str) -> int: + return state.count_group(relic, world.player) + + +# Only use for rifts +def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: + entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) + if not state.can_reach(entrance.connected_region, "Region", world.player): + return False + + if "Free Roam" in entrance.connected_region.name: + return True + + name: str = f"Act Completion ({entrance.connected_region.name})" + return world.multiworld.get_location(name, world.player).access_rule(state) + + +def can_clear_alpine(state: CollectionState, world: World) -> bool: + return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \ + and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player) + + +def can_clear_metro(state: CollectionState, world: World) -> bool: + return state.has("Nyakuza Intro Cleared", world.player) \ + and state.has("Yellow Overpass Station Cleared", world.player) \ + and state.has("Yellow Overpass Manhole Cleared", world.player) \ + and state.has("Green Clean Station Cleared", world.player) \ + and state.has("Green Clean Manhole Cleared", world.player) \ + and state.has("Bluefin Tunnel Cleared", world.player) \ + and state.has("Pink Paw Station Cleared", world.player) \ + and state.has("Pink Paw Manhole Cleared", world.player) + + +def set_rules(world: World): + # First, chapter access + starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value) + world.set_chapter_cost(starting_chapter, 0) + + # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale + chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] + + final_chapter = ChapterIndex.FINALE + if world.multiworld.EndGoal[world.player].value == 2: + final_chapter = ChapterIndex.METRO + chapter_list.append(ChapterIndex.FINALE) + elif world.multiworld.EndGoal[world.player].value == 3: + final_chapter = None + chapter_list.append(ChapterIndex.FINALE) + + if world.is_dlc1(): + chapter_list.append(ChapterIndex.CRUISE) + + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.append(ChapterIndex.METRO) + + chapter_list.remove(starting_chapter) + world.random.shuffle(chapter_list) + + if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): + index1: int = 69 + index2: int = 69 + pos: int + lowest_index: int + chapter_list.remove(ChapterIndex.ALPINE) + + if world.is_dlc1(): + index1 = chapter_list.index(ChapterIndex.CRUISE) + + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + index2 = chapter_list.index(ChapterIndex.METRO) + + lowest_index = min(index1, index2) + if lowest_index == 0: + pos = 0 + else: + pos = world.random.randint(0, lowest_index) + + chapter_list.insert(pos, ChapterIndex.ALPINE) + + if world.is_dlc1() and world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.remove(ChapterIndex.METRO) + index = chapter_list.index(ChapterIndex.CRUISE) + if index >= len(chapter_list): + chapter_list.append(ChapterIndex.METRO) + else: + chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) + + lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value + highest_cost: int = world.multiworld.HighestChapterCost[world.player].value + + cost_increment: int = world.multiworld.ChapterCostIncrement[world.player].value + min_difference: int = world.multiworld.ChapterCostMinDifference[world.player].value + last_cost: int = 0 + cost: int + loop_count: int = 0 + + for chapter in chapter_list: + min_range: int = lowest_cost + (cost_increment * loop_count) + if min_range >= highest_cost: + min_range = highest_cost-1 + + value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) + + cost = world.random.randint(value, min(value + cost_increment, highest_cost)) + if loop_count >= 1: + if last_cost + min_difference > cost: + cost = last_cost + min_difference + + cost = min(cost, highest_cost) + world.set_chapter_cost(chapter, cost) + last_cost = cost + loop_count += 1 + + if final_chapter is not None: + world.set_chapter_cost(final_chapter, world.random.randint( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value)) + + add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) + + add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.SUBCON))) + + add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)) + and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) + + if world.is_dlc1(): + add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE))) + + if world.is_dlc2(): + add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) + and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) + + if world.multiworld.ActRandomizer[world.player].value == 0: + set_default_rift_rules(world) + + table = location_table | event_locs + location: Location + for (key, data) in table.items(): + if not is_location_valid(world, key): + continue + + if key in contract_locations.keys(): + continue + + if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys(): + key = f"{key} ({data.region})" + + location = world.multiworld.get_location(key, world.player) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) + + if data.hookshot: + add_rule(location, lambda state: can_use_hookshot(state, world)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(location, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(location, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + for misc in data.misc_required: + add_rule(location, lambda state, item=misc: state.has(item, world.player)) + + set_specific_rules(world) + + # Putting all of this here, so it doesn't get overridden by anything + # Illness starts the player past the intro + alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player) + add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world)) + if world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player)) + + if zipline_logic(world): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) + + add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) + and state.has("Zipline Unlock - The Lava Cake Path", world.player) + and state.has("Zipline Unlock - The Windmill Path", world.player)) + + if zipline_logic(world): + for (loc, zipline) in zipline_unlocks.items(): + add_rule(world.multiworld.get_location(loc, world.player), + lambda state, z=zipline: state.has(z, world.player)) + + for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: + if "Goat Village" in loc.name: + continue + + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for (key, acts) in act_connections.items(): + if "Arctic Cruise" in key and not world.is_dlc1(): + continue + + i: int = 1 + entrance: Entrance = world.multiworld.get_entrance(key, world.player) + region: Region = entrance.connected_region + access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] + entrance.parent_region.exits.remove(entrance) + + # Entrances to this act that we have to set access_rules on + entrances: typing.List[Entrance] = [] + + for act in acts: + act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) + access_rules.append(act_entrance.access_rule) + required_region = act_entrance.connected_region + name: str = f"{key}: Connection {i}" + new_entrance: Entrance = connect_regions(required_region, region, name, world.player) + entrances.append(new_entrance) + + # Copy access rules from act completions + if "Free Roam" not in required_region.name: + rule: typing.Callable[[CollectionState], bool] + name = f"Act Completion ({required_region.name})" + rule = world.multiworld.get_location(name, world.player).access_rule + access_rules.append(rule) + + i += 1 + + for e in entrances: + for rules in access_rules: + add_rule(e, rules) + + set_event_rules(world) + + if world.multiworld.EndGoal[world.player].value == 1: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) + elif world.multiworld.EndGoal[world.player].value == 2: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) + + +def set_specific_rules(world: World): + add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), + lambda state: state.has("Time Piece", world.player, 12) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), + lambda state: state.has("Time Piece", world.player, 4)) + + set_mafia_town_rules(world) + set_botb_rules(world) + set_subcon_rules(world) + set_alps_rules(world) + + if world.is_dlc1(): + set_dlc1_rules(world) + + if world.is_dlc2(): + set_dlc2_rules(world) + + difficulty: Difficulty = get_difficulty(world) + + if difficulty >= Difficulty.MODERATE: + set_moderate_rules(world) + + if difficulty >= Difficulty.HARD: + set_hard_rules(world) + + if difficulty >= 2: + set_expert_rules(world) + + +def set_moderate_rules(world: World): + # Moderate: Gallery without Brewing Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) + + # Moderate: Above Boats via Ice Hat Sliding + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + + # Moderate: Clock Tower Chest + Ruined Tower with nothing + add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) + + # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell + for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: + set_rule(loc, lambda state: has_paintings(state, world, 1)) + + # Moderate: Vanessa Manor with nothing + for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True) + + # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat + set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world)) + set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access + set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), + lambda state: True) + + # Moderate: Twilight Path without Dweller Mask + set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) + + # Moderate: Mystifying Time Mesa time trial without hats + set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: Finale without Hookshot + set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) + + if world.is_dlc1(): + # Moderate: clear Rock the Boat without Ice Hat + add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) + + # Moderate: clear Deep Sea without Ice Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. + # Yellow Overpass time piece can also be reached without Hookshot quite easily. + if world.is_dlc2(): + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) + + # The player can quite literally walk past the fan from the side without Time Stop. + set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) + + # Moderate: clear Rush Hour without Hookshot + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and can_use_hat(state, world, HatType.ICE) + and can_use_hat(state, world, HatType.BREWING)) + + # Moderate: Bluefin Tunnel without tickets + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + + +def set_hard_rules(world: World): + # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only + add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + # No Dweller Mask required + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), + lambda state: has_paintings(state, world, 3)) + + # Cherry bridge over boss arena gap (painting still expected) + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # SDJ + add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: can_sdj(state, world), "or") + + # Finale Telescope with only Ice Hat + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + + if world.is_dlc1(): + # Hard: clear Deep Sea without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world)) + + if world.is_dlc2(): + # Hard: clear Green Clean Manhole without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) + + # Hard: clear Rush Hour with Brewing Hat only + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + + +def set_expert_rules(world: World): + # Finale Telescope with no hats + set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) + + # Expert: Mafia Town - Above Boats with nothing + set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + + # Expert: Clear Dead Bird Studio with nothing + for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True) + + # Expert: get to and clear Twilight Bell without Dweller Mask. + # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act. + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hookshot(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER) + or can_use_hat(state, world, HatType.SPRINT) + or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player))) + + # Expert: Time Rift - Curly Tail Trail with nothing + # Time Rift - Twilight Bell and Time Rift - Village with nothing + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: True) + + # Expert: Cherry Hovering + entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), + world.multiworld.get_region("Subcon Forest Area", world.player), + "Subcon Forest Entrance YCHE", world.player) + + if world.multiworld.NoPaintingSkips[world.player].value > 0: + add_rule(entrance, lambda state: has_paintings(state, world, 1)) + + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, True)) + + # Set painting rules only. Skipping paintings is determined in has_paintings + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) + + # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him + connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player), + world.multiworld.get_region("Your Contract has Expired", world.player), + "Snatcher Hover", world.player) + set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player), + lambda state: True) + + if world.is_dlc2(): + # Expert: clear Rush Hour with nothing + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + + +def set_mafia_town_rules(world: World): + add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # Old guys don't appear in SCFOS + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) + + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) + + # Only available outside She Came from Outer Space + add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # Only available outside Down with the Mafia! (for some reason) + add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("She Came from Outer Space", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # For some reason, the brewing crate is removed in HUMT + add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player), + lambda state: state.has("HUMT Access", world.player), "or") + + # Can bounce across the lava to get this without Hookshot (need to die though) + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: state.has("HUMT Access", world.player), "or") + + ctr_logic: int = world.multiworld.CTRLogic[world.player].value + if ctr_logic == 3: + set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) + elif ctr_logic == 2: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT), "or") + elif ctr_logic == 1: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + +def set_botb_rules(world: World): + if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE: + set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + + +def set_subcon_rules(world: World): + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.DWELLER)) + + # You can't skip over the boss arena wall without cherry hover, so these two need to be set this way + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) + and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # The painting wall can't be skipped without cherry hover, which is Expert + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player), + lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player), + lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player), + lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player), + lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) + + if painting_logic(world): + add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), + lambda state: has_paintings(state, world, 1, False)) + + for key in contract_locations: + if key == "Snatcher's Contract - The Subcon Well": + continue + + add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) + + +def set_alps_rules(world: World): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP)) + + add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), + lambda state: can_clear_alpine(state, world)) + + +def set_dlc1_rules(world: World): + add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), + lambda state: can_use_hookshot(state, world)) + + # This particular item isn't present in Act 3 for some reason, yes in vanilla too + add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), + lambda state: state.can_reach("Bon Voyage!", "Region", world.player) + or state.can_reach("Ship Shape", "Region", world.player)) + + +def set_dlc2_rules(world: World): + add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), + lambda state: state.has("Metro Ticket - Green", world.player) + or state.has("Metro Ticket - Blue", world.player)) + + add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player)) + + add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player), + lambda state: can_clear_metro(state, world)) + + add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) + + for key in shop_locations.keys(): + if "Green Clean Station Thug B" in key and is_location_valid(world, key): + add_rule(world.multiworld.get_location(key, world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player), "or") + + +def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): + reg: Region + entrance: Entrance + if isinstance(region, str): + reg = world.multiworld.get_region(region, world.player) + else: + reg = region + + if isinstance(unlocked_entrance, str): + entrance = world.multiworld.get_entrance(unlocked_entrance, world.player) + else: + entrance = unlocked_entrance + + world.multiworld.register_indirect_condition(reg, entrance) + + +# See randomize_act_entrances in Regions.py +# Called before set_rules +def set_rift_rules(world: World, regions: typing.Dict[str, Region]): + + # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. + for entrance in regions["Time Rift - Gallery"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in regions["Time Rift - The Lab"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in regions["Time Rift - Sewers"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Bazaar"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Mafia of Cooks"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) + + for entrance in regions["Time Rift - The Owl Express"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - The Moon"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Dead Bird Studio"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) + + for entrance in regions["Time Rift - Pipe"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2", + world.player).connected_region, entrance) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in regions["Time Rift - Village"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4", + world.player).connected_region, entrance) + + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in regions["Time Rift - Sleepy Subcon"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) + + for entrance in regions["Time Rift - Curly Tail Trail"].entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) + + for entrance in regions["Time Rift - The Twilight Bell"].entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) + + for entrance in regions["Time Rift - Alpine Skyline"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + + if world.is_dlc1() > 0: + for entrance in regions["Time Rift - Balcony"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + + for entrance in regions["Time Rift - Deep Sea"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) + + if world.is_dlc2() > 0: + for entrance in regions["Time Rift - Rumbi Factory"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) + + +# Basically the same as above, but without the need of the dict since we are just setting defaults +# Called if Act Rando is disabled +def set_default_rift_rules(world: World): + + for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, "Down with the Mafia!", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, "Heating Up Mafia Town", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) + + for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, "Murder on the Owl Express", entrance.name) + reg_act_connection(world, "Picture Perfect", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, "Train Rush", entrance.name) + reg_act_connection(world, "The Big Parade", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) + + for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, "The Subcon Well", entrance.name) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, "Queen Vanessa's Manor", entrance.name) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) + + for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) + + for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) + + for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + + if world.is_dlc1(): + for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + + for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) + + if world.is_dlc2(): + for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) + + +def set_event_rules(world: World): + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + if data.dlc_flags & HatDLC.death_wish and name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + + event: Location = world.multiworld.get_location(name, world.player) + + if data.act_event: + add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule) + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py new file mode 100644 index 0000000000..16255d7ec5 --- /dev/null +++ b/worlds/ahit/Types.py @@ -0,0 +1,80 @@ +from enum import IntEnum, IntFlag +from typing import NamedTuple, Optional, List +from BaseClasses import Location, Item, ItemClassification + + +class HatInTimeLocation(Location): + game: str = "A Hat in Time" + + +class HatInTimeItem(Item): + game: str = "A Hat in Time" + + +class HatType(IntEnum): + NONE = -1 + SPRINT = 0 + BREWING = 1 + ICE = 2 + DWELLER = 3 + TIME_STOP = 4 + + +class HatDLC(IntFlag): + none = 0b000 + dlc1 = 0b001 + dlc2 = 0b010 + death_wish = 0b100 + dlc1_dw = 0b101 + dlc2_dw = 0b110 + + +class ChapterIndex(IntEnum): + SPACESHIP = 0 + MAFIA = 1 + BIRDS = 2 + SUBCON = 3 + ALPINE = 4 + FINALE = 5 + CRUISE = 6 + METRO = 7 + + +class Difficulty(IntEnum): + NORMAL = -1 + MODERATE = 0 + HARD = 1 + EXPERT = 2 + + +class LocData(NamedTuple): + id: Optional[int] = 0 + region: Optional[str] = "" + required_hats: Optional[List[HatType]] = [HatType.NONE] + hookshot: Optional[bool] = False + dlc_flags: Optional[HatDLC] = HatDLC.none + paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle + misc_required: Optional[List[str]] = [] + + # For UmbrellaLogic setting + umbrella: Optional[bool] = False # Umbrella required for this check + hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) + + # Other + act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion + nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) + + +class ItemData(NamedTuple): + code: Optional[int] + classification: ItemClassification + dlc_flags: Optional[HatDLC] = HatDLC.none + + +hat_type_to_item = { + HatType.SPRINT: "Sprint Hat", + HatType.BREWING: "Brewing Hat", + HatType.ICE: "Ice Hat", + HatType.DWELLER: "Dweller Mask", + HatType.TIME_STOP: "Time Stop Hat", +} diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py new file mode 100644 index 0000000000..0ed14c6376 --- /dev/null +++ b/worlds/ahit/__init__.py @@ -0,0 +1,334 @@ +from BaseClasses import Item, ItemClassification, Tutorial +from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool +from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID +from .Rules import set_rules +from .Options import ahit_options, slot_data_options, adjust_options +from .Types import HatType, ChapterIndex, HatInTimeItem +from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes +from .DeathWishRules import set_dw_rules, create_enemy_events +from worlds.AutoWorld import World, WebWorld +from typing import List, Dict, TextIO +from worlds.LauncherComponents import Component, components, icon_paths +from Utils import local_path + +hat_craft_order: Dict[int, List[HatType]] = {} +hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} +chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} +excluded_dws: Dict[int, List[str]] = {} +excluded_bonuses: Dict[int, List[str]] = {} +dw_shuffle: Dict[int, List[str]] = {} +nyakuza_thug_items: Dict[int, Dict[str, int]] = {} +badge_seller_count: Dict[int, int] = {} + +components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta')) +icon_paths['yatta'] = local_path('data', 'yatta.png') + + +class AWebInTime(WebWorld): + theme = "partyTime" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide for setting up A Hat in Time to be played in Archipelago.", + "English", + "ahit_en.md", + "setup/en", + ["CookieCat"] + )] + + +class HatInTimeWorld(World): + """ + A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers! + Freely explore giant worlds and recover Time Pieces to travel to new heights! + """ + + game = "A Hat in Time" + data_version = 1 + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = get_location_names() + + option_definitions = ahit_options + act_connections: Dict[str, str] = {} + shop_locs: List[str] = [] + item_name_groups = relic_groups + web = AWebInTime() + + def generate_early(self): + adjust_options(self) + + if self.multiworld.StartWithCompassBadge[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Compass Badge")) + + if self.is_dw_only(): + return + + # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory + # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock + start_chapter: int = self.multiworld.StartingChapter[self.player].value + + if start_chapter == 4 or start_chapter == 3: + if self.multiworld.ActRandomizer[self.player].value == 0: + if start_chapter == 4: + self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + + if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) + + def create_regions(self): + excluded_dws[self.player] = [] + excluded_bonuses[self.player] = [] + dw_shuffle[self.player] = [] + nyakuza_thug_items[self.player] = {} + badge_seller_count[self.player] = 0 + self.shop_locs = [] + self.topology_present = self.multiworld.ActRandomizer[self.player].value + + create_regions(self) + + if self.multiworld.EnableDeathWish[self.player].value > 0: + create_dw_regions(self) + + if self.is_dw_only(): + return + + create_events(self) + if self.is_dw(): + if "Snatcher's Hit List" not in self.get_excluded_dws() \ + or "Camera Tourist" not in self.get_excluded_dws(): + create_enemy_events(self) + + # place vanilla contract locations if contract shuffle is off + if self.multiworld.ShuffleActContracts[self.player].value == 0: + for name in contract_locations.keys(): + self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) + + def create_items(self): + hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} + + hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, + HatType.DWELLER, HatType.TIME_STOP] + + if self.multiworld.HatItems[self.player].value == 0 and self.multiworld.RandomizeHatOrder[self.player].value > 0: + self.random.shuffle(hat_craft_order[self.player]) + if self.multiworld.RandomizeHatOrder[self.player].value == 2: + hat_craft_order[self.player].remove(HatType.TIME_STOP) + hat_craft_order[self.player].append(HatType.TIME_STOP) + + self.multiworld.itempool += create_itempool(self) + + def set_rules(self): + self.act_connections = {} + chapter_timepiece_costs[self.player] = {ChapterIndex.MAFIA: -1, + ChapterIndex.BIRDS: -1, + ChapterIndex.SUBCON: -1, + ChapterIndex.ALPINE: -1, + ChapterIndex.FINALE: -1, + ChapterIndex.CRUISE: -1, + ChapterIndex.METRO: -1} + + if self.is_dw_only(): + # we already have all items if this is the case, no need for rules + self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression, + None, self.player)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", + self.player) + + if self.multiworld.DWEnableBonus[self.player].value == 0: + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): + continue + + if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle(): + continue + + full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player)) + full_clear.show_in_spoiler = False + + return + + if self.multiworld.ActRandomizer[self.player].value > 0: + randomize_act_entrances(self) + + set_rules(self) + + if self.is_dw(): + set_dw_rules(self) + + def create_item(self, name: str) -> Item: + return create_item(self, name) + + def fill_slot_data(self) -> dict: + slot_data: dict = {"Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], + "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], + "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], + "Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE], + "Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE], + "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], + "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], + "BadgeSellerItemCount": badge_seller_count[self.player], + "SeedNumber": str(self.multiworld.seed), # For shop prices + "SeedName": self.multiworld.seed_name} + + if self.multiworld.HatItems[self.player].value == 0: + slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) + slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING]) + slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE]) + slot_data.setdefault("DwellerYarnCost", hat_yarn_costs[self.player][HatType.DWELLER]) + slot_data.setdefault("TimeStopYarnCost", hat_yarn_costs[self.player][HatType.TIME_STOP]) + slot_data.setdefault("Hat1", int(hat_craft_order[self.player][0])) + slot_data.setdefault("Hat2", int(hat_craft_order[self.player][1])) + slot_data.setdefault("Hat3", int(hat_craft_order[self.player][2])) + slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3])) + slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4])) + + if self.multiworld.ActRandomizer[self.player].value > 0: + for name in self.act_connections.keys(): + slot_data[name] = self.act_connections[name] + + if self.is_dlc2() and not self.is_dw_only(): + for name in nyakuza_thug_items[self.player].keys(): + slot_data[name] = nyakuza_thug_items[self.player][name] + + if self.is_dw(): + i: int = 0 + for name in excluded_dws[self.player]: + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal": + continue + + slot_data[f"excluded_dw{i}"] = dw_classes[name] + i += 1 + + i = 0 + if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0: + for name in excluded_bonuses[self.player]: + if name in excluded_dws[self.player]: + continue + + slot_data[f"excluded_bonus{i}"] = dw_classes[name] + i += 1 + + if self.multiworld.DWShuffle[self.player].value > 0: + shuffled_dws = self.get_dw_shuffle() + for i in range(len(shuffled_dws)): + slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] + + for option_name in slot_data_options: + option = getattr(self.multiworld, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.is_dw_only() or self.multiworld.ActRandomizer[self.player].value == 0: + return + + new_hint_data = {} + alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] + metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] + + for key, data in location_table.items(): + if not is_location_valid(self, key): + continue + + location = self.multiworld.get_location(key, self.player) + region_name: str + + if data.region in alpine_regions: + region_name = "Alpine Free Roam" + elif data.region in metro_regions: + region_name = "Nyakuza Free Roam" + elif data.region in chapter_act_info.keys(): + region_name = location.parent_region.name + else: + continue + + new_hint_data[location.address] = get_shuffled_region(self, region_name) + + if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: + ship_shape_region = get_shuffled_region(self, "Ship Shape") + id_start: int = TASKSANITY_START_ID + for i in range(self.multiworld.TasksanityCheckCount[self.player].value): + new_hint_data[id_start+i] = ship_shape_region + + hint_data[self.player] = new_hint_data + + def write_spoiler_header(self, spoiler_handle: TextIO): + for i in self.get_chapter_costs(): + spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) + + for hat in hat_craft_order[self.player]: + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) + + def set_chapter_cost(self, chapter: ChapterIndex, cost: int): + chapter_timepiece_costs[self.player][chapter] = cost + + def get_chapter_cost(self, chapter: ChapterIndex) -> int: + return chapter_timepiece_costs[self.player][chapter] + + def get_hat_craft_order(self): + return hat_craft_order[self.player] + + def get_hat_yarn_costs(self): + return hat_yarn_costs[self.player] + + def get_chapter_costs(self): + return chapter_timepiece_costs[self.player] + + def is_dlc1(self) -> bool: + return self.multiworld.EnableDLC1[self.player].value > 0 + + def is_dlc2(self) -> bool: + return self.multiworld.EnableDLC2[self.player].value > 0 + + def is_dw(self) -> bool: + return self.multiworld.EnableDeathWish[self.player].value > 0 + + def is_dw_only(self) -> bool: + return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0 + + def get_excluded_dws(self): + return excluded_dws[self.player] + + def get_excluded_bonuses(self): + return excluded_bonuses[self.player] + + def is_dw_excluded(self, name: str) -> bool: + # don't exclude Seal the Deal if it's our goal + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \ + and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: + return False + + if name in excluded_dws[self.player]: + return True + + return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player] + + def is_bonus_excluded(self, name: str) -> bool: + if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: + return True + + return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player] + + def get_dw_shuffle(self): + return dw_shuffle[self.player] + + def set_dw_shuffle(self, shuffle: List[str]): + dw_shuffle[self.player] = shuffle + + def get_badge_seller_count(self) -> int: + return badge_seller_count[self.player] + + def set_badge_seller_count(self, value: int): + badge_seller_count[self.player] = value + + def get_nyakuza_thug_items(self): + return nyakuza_thug_items[self.player] + + def set_nyakuza_thug_items(self, items: Dict[str, int]): + nyakuza_thug_items[self.player] = items diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md new file mode 100644 index 0000000000..c4a4341763 --- /dev/null +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -0,0 +1,31 @@ +# A Hat in Time + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. + +To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to enter that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. + +## What items and locations get shuffled? + +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are the locations. + +Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in A Hat in Time? + +Items belonging to other worlds are represented by a badge with the Archipelago logo on it. + +## When the player receives an item, what happens? + +When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console. diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md new file mode 100644 index 0000000000..d2db2fe47f --- /dev/null +++ b/worlds/ahit/docs/setup_en.md @@ -0,0 +1,43 @@ +# Setup Guide for A Hat in Time in Archipelago + +## Required Software +- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/) + +- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601) + + +## Instructions + +1. Have Steam running. Open the Steam console with [this link.](steam://open/console) + +2. In the Steam console, enter the following command: +`download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. + +3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. + +4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. + +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line. + +6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. + +7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. + + + +## Connecting to the Archipelago server + +When you create a new save file, you should be prompted to enter your slot name, password, and Archipelago server address:port after loading into the Spaceship. Once that's done, the game will automatically connect to the multiserver using the info you entered whenever that save file is loaded. If you must change the IP or port for the save file, use the `ap_set_connection_info` console command. + + +## Console Commands + +Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. + +`ap_say ` - Send a chat message to the server. Supports commands, such as !hint or !release. + +`ap_deathlink` - Toggle Death Link. + +`ap_set_connection_info ` - Set the connection info for the save file. The IP address MUST BE IN QUOTES! + +`ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py new file mode 100644 index 0000000000..7c2b9783e6 --- /dev/null +++ b/worlds/ahit/test/TestActs.py @@ -0,0 +1,31 @@ +from worlds.ahit.Regions import act_chapters +from worlds.ahit.test.TestBase import HatInTimeTestBase + + +class TestActs(HatInTimeTestBase): + def run_default_tests(self) -> bool: + return False + + def testAllStateCanReachEverything(self): + pass + + options = { + "ActRandomizer": 2, + "EnableDLC1": 1, + "EnableDLC2": 1, + "ShuffleActContracts": 0, + } + + def test_act_shuffle(self): + for i in range(1000): + self.world_setup() + self.collect_all_but([""]) + + for name in act_chapters.keys(): + region = self.multiworld.get_region(name, 1) + for entrance in region.entrances: + self.assertTrue(self.can_reach_entrance(entrance.name), + f"Can't reach {name} from {entrance}\n" + f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} " + f"-> {entrance} -> {name}" + f" (expected method of access)") diff --git a/worlds/ahit/test/TestBase.py b/worlds/ahit/test/TestBase.py new file mode 100644 index 0000000000..1eb4dd6555 --- /dev/null +++ b/worlds/ahit/test/TestBase.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time" diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 58fcd50721dc773c5b90c5ac40ef9b68552f3fb1 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:42:07 -0500 Subject: [PATCH 6/8] 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. From f4f405fc764d56ff1ec425542e105d19b259af7c Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:44:21 -0500 Subject: [PATCH 7/8] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b9ca4b8d28..361ea34407 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage -/oot/ # Byte-compiled / optimized / DLL files __pycache__/ From 74715c397cbcfac1314df26542cd94aa191fa96a Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 14:09:29 -0500 Subject: [PATCH 8/8] 1.3.6 --- worlds/ahit/DeathWishLocations.py | 1 + worlds/ahit/Locations.py | 4 +- worlds/ahit/Options.py | 11 +++++ worlds/ahit/Regions.py | 10 ++--- worlds/ahit/Rules.py | 67 ++++++++++++++++++++++++------- worlds/ahit/__init__.py | 6 ++- worlds/ahit/test/TestActs.py | 7 ++-- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index f51d4948ee..bc529261c1 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -197,6 +197,7 @@ def create_dw_regions(world: World): if i == 0: connect_regions(dw_map, dw, f"-> {name}", world.player) else: + # noinspection PyUnboundLocalVariable connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) loc_id = death_wishes[name] diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index bf31c8cba8..2f47c2ebc0 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -283,7 +283,7 @@ ahit_locations = { # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", @@ -900,6 +900,8 @@ event_locs = { "HUMT Access": LocData(0, "Heating Up Mafia Town"), "TOD Access": LocData(0, "Toilet of Doom"), "YCHE Access": LocData(0, "Your Contract has Expired"), + "AFR Access": LocData(0, "Alpine Free Roam"), + "TIHS Access": LocData(0, "The Illness has Spread"), "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index f3dd2a8c66..18d93802a1 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -458,6 +458,15 @@ class NyakuzaThugMaxShopItems(Range): default = 4 +class NoTicketSkips(Choice): + """Prevent metro gate skips from being in logic on higher difficulties. + Rush Hour option will only consider the ticket skips for Rush Hour in logic.""" + display_name = "No Ticket Skips" + option_false = 0 + option_true = 1 + option_rush_hour = 2 + + class BaseballBat(Toggle): """Replace the Umbrella with the baseball bat from Nyakuza Metro. DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" @@ -656,6 +665,7 @@ ahit_options: typing.Dict[str, type(Option)] = { "MetroMaxPonCost": MetroMaxPonCost, "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + "NoTicketSkips": NoTicketSkips, "LowestChapterCost": LowestChapterCost, "HighestChapterCost": HighestChapterCost, @@ -720,6 +730,7 @@ slot_data_options: typing.Dict[str, type(Option)] = { "MetroMinPonCost": MetroMinPonCost, "MetroMaxPonCost": MetroMaxPonCost, "BaseballBat": BaseballBat, + "NoTicketSkips": NoTicketSkips, "MinPonCost": MinPonCost, "MaxPonCost": MaxPonCost, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 807f1ee77f..0737f880be 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -309,10 +309,10 @@ def create_regions(world: World): # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) - post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + post_ev = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): - connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + connect_regions(basement, post_ev, "DBS Basement -> Post Elevator Area", p) # ------------------------------------------- SUBCON FOREST --------------------------------------- # subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) @@ -501,12 +501,12 @@ def randomize_act_entrances(world: World): region_list.append(region) for region in region_list.copy(): - if "Time Rift" in region.name: + if region.name in chapter_finales: region_list.remove(region) region_list.append(region) for region in region_list.copy(): - if region.name in chapter_finales: + if "Time Rift" in region.name: region_list.remove(region) region_list.append(region) @@ -631,8 +631,8 @@ def randomize_act_entrances(world: World): candidate = c break + # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) - # print(region, candidate) # Vanilla if candidate.name == region.name: diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index 7eb09bedfc..b50a7cdf35 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -317,9 +317,24 @@ def set_rules(world: World): for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: if "Goat Village" in loc.name: continue + # This needs some special handling + if loc.name == "Alpine Skyline - Goat Refinery": + add_rule(loc, lambda state: state.has("AFR Access", world.player) + and can_use_hookshot(state, world) + and can_hit(state, world, True)) + + difficulty: Difficulty = Difficulty(world.multiworld.LogicDifficulty[world.player].value) + if difficulty >= Difficulty.MODERATE: + add_rule(loc, lambda state: state.has("TIHS Access", world.player) + and can_use_hat(state, world, HatType.SPRINT), "or") + elif difficulty >= Difficulty.HARD: + add_rule(loc, lambda state: state.has("TIHS Access", world.player, "or")) + + continue add_rule(loc, lambda state: can_use_hookshot(state, world)) + dummy_entrances: typing.List[Entrance] = [] for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): continue @@ -328,7 +343,7 @@ def set_rules(world: World): entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] - entrance.parent_region.exits.remove(entrance) + dummy_entrances.append(entrance) # Entrances to this act that we have to set access_rules on entrances: typing.List[Entrance] = [] @@ -354,6 +369,9 @@ def set_rules(world: World): for rules in access_rules: add_rule(e, rules) + for e in dummy_entrances: + set_rule(e, lambda state: False) + set_event_rules(world) if world.multiworld.EndGoal[world.player].value == 1: @@ -448,13 +466,12 @@ def set_moderate_rules(world: World): # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. # Yellow Overpass time piece can also be reached without Hookshot quite easily. if world.is_dlc2(): - set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + # No Hookshot set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), lambda state: True) + # No Dweller, Hookshot, or Time Stop for these set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) - - # The player can quite literally walk past the fan from the side without Time Stop. set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) # Moderate: clear Rush Hour without Hookshot @@ -465,8 +482,10 @@ def set_moderate_rules(world: World): and can_use_hat(state, world, HatType.ICE) and can_use_hat(state, world, HatType.BREWING)) - # Moderate: Bluefin Tunnel without tickets - set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + # Moderate: Bluefin Tunnel + Pink Paw Station without tickets + if world.multiworld.NoTicketSkips[world.player].value == 0: + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) def set_hard_rules(world: World): @@ -483,6 +502,13 @@ def set_hard_rules(world: World): set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) + # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") @@ -508,8 +534,15 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + if world.multiworld.NoTicketSkips[world.player].value != 1: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + else: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) def set_expert_rules(world: World): @@ -517,8 +550,10 @@ def set_expert_rules(world: World): set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) - # Expert: Mafia Town - Above Boats with nothing + # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True) # Expert: Clear Dead Bird Studio with nothing for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: @@ -561,13 +596,9 @@ def set_expert_rules(world: World): # Set painting rules only. Skipping paintings is determined in has_paintings set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), lambda state: has_paintings(state, world, 1, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), - lambda state: has_paintings(state, world, 2, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: has_paintings(state, world, 2, True)) set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), lambda state: has_paintings(state, world, 3, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), lambda state: has_paintings(state, world, 3, True)) # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him @@ -579,7 +610,13 @@ def set_expert_rules(world: World): if world.is_dlc2(): # Expert: clear Rush Hour with nothing - set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + if world.multiworld.NoTicketSkips[world.player].value == 0: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + else: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) def set_mafia_town_rules(world: World): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 0ed14c6376..805dc57898 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,7 +1,8 @@ from BaseClasses import Item, ItemClassification, Tutorial from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region -from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ + get_total_locations from .Rules import set_rules from .Options import ahit_options, slot_data_options, adjust_options from .Types import HatType, ChapterIndex, HatInTimeItem @@ -173,7 +174,8 @@ class HatInTimeWorld(World): "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], "BadgeSellerItemCount": badge_seller_count[self.player], "SeedNumber": str(self.multiworld.seed), # For shop prices - "SeedName": self.multiworld.seed_name} + "SeedName": self.multiworld.seed_name, + "TotalLocations": get_total_locations(self)} if self.multiworld.HatItems[self.player].value == 0: slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 7c2b9783e6..da3d5f5c0c 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -1,4 +1,5 @@ from worlds.ahit.Regions import act_chapters +from worlds.ahit.Rules import act_connections from worlds.ahit.test.TestBase import HatInTimeTestBase @@ -6,9 +7,6 @@ class TestActs(HatInTimeTestBase): def run_default_tests(self) -> bool: return False - def testAllStateCanReachEverything(self): - pass - options = { "ActRandomizer": 2, "EnableDLC1": 1, @@ -24,6 +22,9 @@ class TestActs(HatInTimeTestBase): for name in act_chapters.keys(): region = self.multiworld.get_region(name, 1) for entrance in region.entrances: + if entrance.name in act_connections.keys(): + continue + self.assertTrue(self.can_reach_entrance(entrance.name), f"Can't reach {name} from {entrance}\n" f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} "