mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-27 16:43:28 -07:00
Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit"
This reverts commita2360fe197, reversing changes made tob8948bc495.
This commit is contained in:
218
BaseClasses.py
218
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:
|
||||
"""
|
||||
|
||||
45
Fill.py
45
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')
|
||||
|
||||
17
Generate.py
17
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)
|
||||
|
||||
7
Main.py
7
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")
|
||||
|
||||
@@ -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
|
||||
|
||||
64
Utils.py
64
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)
|
||||
|
||||
@@ -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=(',', ': '))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
{%- endif -%}
|
||||
{% endif %}
|
||||
{%- endfor -%}
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
<td class="center-column">{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}</td>
|
||||
{%- if activity_timers[(team, player)] -%}
|
||||
<td class="center-column">{{ activity_timers[(team, player)].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<td class="center-column" data-sort="{{ checks["Total"] }}">
|
||||
{{ checks["Total"] }}/{{ locations[player] | length }}
|
||||
</td>
|
||||
<td class="center-column">{{ percent_total_checks_done[team][player] }}</td>
|
||||
<td class="center-column">{{ "{0:.2f}".format(percent_total_checks_done[team][player]) }}</td>
|
||||
{%- if activity_timers[team, player] -%}
|
||||
<td class="center-column">{{ activity_timers[team, player].total_seconds() }}</td>
|
||||
{%- else -%}
|
||||
@@ -72,7 +72,13 @@
|
||||
<td>All Games</td>
|
||||
<td>{{ completed_worlds }}/{{ players|length }} Complete</td>
|
||||
<td class="center-column">{{ players.values()|sum(attribute='Total') }}/{{ total_locations[team] }}</td>
|
||||
<td class="center-column">{{ (players.values()|sum(attribute='Total') / total_locations[team] * 100) | int }}</td>
|
||||
<td class="center-column">
|
||||
{% if total_locations[team] == 0 %}
|
||||
100
|
||||
{% else %}
|
||||
{{ "{0:.2f}".format(players.values()|sum(attribute='Total') / total_locations[team] * 100) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="center-column last-activity"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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] = []
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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|
|
||||
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|
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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"]]]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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_)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user