mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 14:13:21 -07:00
Merge branch 'main' into civ6-1.0
This commit is contained in:
@@ -11,8 +11,10 @@ from argparse import Namespace
|
||||
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, Mapping, NamedTuple, Optional, Set, Tuple, \
|
||||
TypedDict, Union, Type, ClassVar
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, Type)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
import NetUtils
|
||||
import Options
|
||||
@@ -22,16 +24,16 @@ if typing.TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
class Group(TypedDict):
|
||||
name: str
|
||||
game: str
|
||||
world: "AutoWorld.World"
|
||||
players: Set[int]
|
||||
item_pool: Set[str]
|
||||
replacement_items: Dict[int, Optional[str]]
|
||||
local_items: Set[str]
|
||||
non_local_items: Set[str]
|
||||
link_replacement: bool
|
||||
players: AbstractSet[int]
|
||||
item_pool: NotRequired[Set[str]]
|
||||
replacement_items: NotRequired[Dict[int, Optional[str]]]
|
||||
local_items: NotRequired[Set[str]]
|
||||
non_local_items: NotRequired[Set[str]]
|
||||
link_replacement: NotRequired[bool]
|
||||
|
||||
|
||||
class ThreadBarrierProxy:
|
||||
@@ -48,6 +50,11 @@ class ThreadBarrierProxy:
|
||||
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
||||
|
||||
|
||||
class HasNameAndPlayer(Protocol):
|
||||
name: str
|
||||
player: int
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_name: Dict[int, str]
|
||||
@@ -156,7 +163,7 @@ class MultiWorld():
|
||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
def set_player_attr(attr: str, val) -> None:
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
@@ -165,13 +172,13 @@ class MultiWorld():
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
"world's random object instead (usually self.random)")
|
||||
"world's random object instead (usually self.random)")
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
return self.player_ids + tuple(self.groups)
|
||||
|
||||
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
|
||||
"""Create a group with name and return the assigned player ID and group.
|
||||
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
||||
from worlds import AutoWorld
|
||||
@@ -195,7 +202,7 @@ class MultiWorld():
|
||||
|
||||
return new_id, new_group
|
||||
|
||||
def get_player_groups(self, player) -> Set[int]:
|
||||
def get_player_groups(self, player: int) -> Set[int]:
|
||||
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
||||
|
||||
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||
@@ -258,7 +265,7 @@ class MultiWorld():
|
||||
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
||||
}
|
||||
|
||||
for name, item_link in item_links.items():
|
||||
for _name, item_link in item_links.items():
|
||||
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
||||
pool = set()
|
||||
local_items = set()
|
||||
@@ -388,7 +395,7 @@ class MultiWorld():
|
||||
return tuple(world for player, world in self.worlds.items() if
|
||||
player not in self.groups and self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
def get_player_name(self, player: int) -> str:
|
||||
@@ -439,7 +446,7 @@ class MultiWorld():
|
||||
def get_items(self) -> List[Item]:
|
||||
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
||||
|
||||
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
if resolve_group_locations:
|
||||
player_groups = self.get_player_groups(player)
|
||||
return [location for location in self.get_locations() if
|
||||
@@ -448,7 +455,7 @@ class MultiWorld():
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player]
|
||||
|
||||
def find_item(self, item, player: int) -> Location:
|
||||
def find_item(self, item: str, player: int) -> Location:
|
||||
return next(location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player)
|
||||
|
||||
@@ -806,7 +813,7 @@ class CollectionState():
|
||||
if found >= count:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
||||
Ignores duplicates of the same item."""
|
||||
@@ -821,7 +828,7 @@ class CollectionState():
|
||||
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state."""
|
||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||
|
||||
|
||||
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
||||
@@ -900,7 +907,7 @@ class Entrance:
|
||||
addresses = None
|
||||
target = None
|
||||
|
||||
def __init__(self, player: int, name: str = '', parent: Region = None):
|
||||
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
|
||||
self.name = name
|
||||
self.parent_region = parent
|
||||
self.player = player
|
||||
@@ -920,9 +927,6 @@ class Entrance:
|
||||
region.entrances.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
@@ -1048,7 +1052,7 @@ class Region:
|
||||
self.locations.append(location_type(self.player, location, address, self))
|
||||
|
||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||
"""
|
||||
Connects this Region to another Region, placing the provided rule on the connection.
|
||||
|
||||
@@ -1088,9 +1092,6 @@ class Region:
|
||||
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
@@ -1109,9 +1110,9 @@ class Location:
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda state, item: False)
|
||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||
@@ -1120,11 +1121,15 @@ class Location:
|
||||
self.address = address
|
||||
self.parent_region = parent
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))))
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
||||
return ((
|
||||
self.always_allow(state, item)
|
||||
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
|
||||
) or (
|
||||
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))
|
||||
))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
||||
@@ -1139,9 +1144,6 @@ class Location:
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
@@ -1163,7 +1165,7 @@ class Location:
|
||||
@property
|
||||
def native_item(self) -> bool:
|
||||
"""Returns True if the item in this location matches game."""
|
||||
return self.item and self.item.game == self.game
|
||||
return self.item is not None and self.item.game == self.game
|
||||
|
||||
@property
|
||||
def hint_text(self) -> str:
|
||||
@@ -1246,9 +1248,6 @@ class Item:
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
|
||||
return self.location.parent_region.multiworld.get_name_string_for_object(self)
|
||||
return f"{self.name} (Player {self.player})"
|
||||
@@ -1326,9 +1325,9 @@ class Spoiler:
|
||||
|
||||
# in the second phase, we cull each sphere such that the game is still beatable,
|
||||
# reducing each range of influence to the bare minimum required inside it
|
||||
restore_later = {}
|
||||
restore_later: Dict[Location, Item] = {}
|
||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||
to_delete = set()
|
||||
to_delete: Set[Location] = set()
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||
@@ -1346,7 +1345,7 @@ class Spoiler:
|
||||
sphere -= to_delete
|
||||
|
||||
# second phase, sphere 0
|
||||
removed_precollected = []
|
||||
removed_precollected: List[Item] = []
|
||||
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
multiworld.precollected_items[item.player].remove(item)
|
||||
@@ -1499,9 +1498,9 @@ class Spoiler:
|
||||
|
||||
if self.paths:
|
||||
outfile.write('\n\nPaths:\n\n')
|
||||
path_listings = []
|
||||
path_listings: List[str] = []
|
||||
for location, path in sorted(self.paths.items()):
|
||||
path_lines = []
|
||||
path_lines: List[str] = []
|
||||
for region, exit in path:
|
||||
if exit is not None:
|
||||
path_lines.append("{} -> {}".format(region, exit))
|
||||
|
||||
2
kvui.py
2
kvui.py
@@ -5,6 +5,8 @@ import typing
|
||||
import re
|
||||
from collections import deque
|
||||
|
||||
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
|
||||
|
||||
@@ -132,7 +132,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
||||
break
|
||||
if found_already_loaded:
|
||||
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
|
||||
"so a Launcher restart is required to use the new installation.")
|
||||
"so a Launcher restart is required to use the new installation.\n"
|
||||
"If the Launcher is not open, no action needs to be taken.")
|
||||
world_source = worlds.WorldSource(str(target), is_zip=True)
|
||||
bisect.insort(worlds.world_sources, world_source)
|
||||
world_source.load()
|
||||
|
||||
@@ -28,7 +28,7 @@ An Example `AP.json` file:
|
||||
|
||||
```
|
||||
{
|
||||
"Url": "archipelago:12345",
|
||||
"Url": "archipelago.gg:12345",
|
||||
"SlotName": "Maddy",
|
||||
"Password": ""
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import add_rule
|
||||
from math import ceil
|
||||
|
||||
SINGLE_PUPPIES = ["Puppy " + str(i).rjust(2,"0") for i in range(1,100)]
|
||||
TRIPLE_PUPPIES = ["Puppies " + str(3*(i-1)+1).rjust(2, "0") + "-" + str(3*(i-1)+3).rjust(2, "0") for i in range(1,34)]
|
||||
@@ -28,7 +29,7 @@ def has_puppies_all(state: CollectionState, player: int, puppies_required: int)
|
||||
return state.has("All Puppies", player)
|
||||
|
||||
def has_puppies_triplets(state: CollectionState, player: int, puppies_required: int) -> bool:
|
||||
return state.has_from_list_unique(TRIPLE_PUPPIES, player, -(puppies_required / -3))
|
||||
return state.has_from_list_unique(TRIPLE_PUPPIES, player, ceil(puppies_required / 3))
|
||||
|
||||
def has_puppies_individual(state: CollectionState, player: int, puppies_required: int) -> bool:
|
||||
return state.has_from_list_unique(SINGLE_PUPPIES, player, puppies_required)
|
||||
|
||||
@@ -137,6 +137,8 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
previous_death_link: float
|
||||
ignore_next_death_link: bool
|
||||
|
||||
current_map: Optional[int]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.local_checked_locations = set()
|
||||
@@ -150,6 +152,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
self.death_counter = None
|
||||
self.previous_death_link = 0
|
||||
self.ignore_next_death_link = False
|
||||
self.current_map = None
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from CommonClient import logger
|
||||
@@ -243,6 +246,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
|
||||
|
||||
await self.handle_tracker_info(ctx, guards)
|
||||
await self.handle_death_link(ctx, guards)
|
||||
await self.handle_received_items(ctx, guards)
|
||||
await self.handle_wonder_trade(ctx, guards)
|
||||
@@ -403,6 +407,31 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
# Exit handler and return to main loop to reconnect
|
||||
pass
|
||||
|
||||
async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
# Current map
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(sb1_address + 0x4, 2, "System Bus")],
|
||||
[guards["SAVE BLOCK 1"]]
|
||||
)
|
||||
if read_result is None: # Save block moved
|
||||
return
|
||||
|
||||
current_map = int.from_bytes(read_result[0], "big")
|
||||
if current_map != self.current_map:
|
||||
self.current_map = current_map
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Bounce",
|
||||
"slots": [ctx.slot],
|
||||
"tags": ["Tracker"],
|
||||
"data": {
|
||||
"type": "MapUpdate",
|
||||
"mapId": current_map,
|
||||
},
|
||||
}])
|
||||
|
||||
async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
"""
|
||||
Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game
|
||||
|
||||
@@ -128,10 +128,10 @@ class WitnessWorld(World):
|
||||
)
|
||||
|
||||
if not has_locally_relevant_progression and self.multiworld.players == 1:
|
||||
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
|
||||
warning(f"{self.player_name}'s Witness world doesn't have any progression"
|
||||
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
|
||||
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
|
||||
raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
|
||||
raise OptionError(f"{self.player_name}'s Witness world doesn't have enough"
|
||||
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
|
||||
f" Shuffle, Door Shuffle, or Obelisk Keys.")
|
||||
|
||||
@@ -189,12 +189,13 @@ class WitnessWorld(World):
|
||||
event_locations.append(location_obj)
|
||||
|
||||
# Place other locked items
|
||||
dog_puzzle_skip = self.create_item("Puzzle Skip")
|
||||
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
|
||||
|
||||
self.own_itempool.append(dog_puzzle_skip)
|
||||
if self.options.shuffle_dog == "puzzle_skip":
|
||||
dog_puzzle_skip = self.create_item("Puzzle Skip")
|
||||
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
|
||||
|
||||
self.items_placed_early.append("Puzzle Skip")
|
||||
self.own_itempool.append(dog_puzzle_skip)
|
||||
self.items_placed_early.append("Puzzle Skip")
|
||||
|
||||
if self.options.early_symbol_item:
|
||||
# Pick an early item to place on the tutorial gate.
|
||||
@@ -213,7 +214,7 @@ class WitnessWorld(World):
|
||||
self.own_itempool.append(gate_item)
|
||||
self.items_placed_early.append(random_early_item)
|
||||
|
||||
# There are some really restrictive settings in The Witness.
|
||||
# There are some really restrictive options in The Witness.
|
||||
# They are rarely played, but when they are, we add some extra sphere 1 locations.
|
||||
# This is done both to prevent generation failures, but also to make the early game less linear.
|
||||
# Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange.
|
||||
@@ -221,11 +222,14 @@ class WitnessWorld(World):
|
||||
state = CollectionState(self.multiworld)
|
||||
state.sweep_for_advancements(locations=event_locations)
|
||||
|
||||
num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address)
|
||||
num_early_locs = sum(
|
||||
1 for loc in self.multiworld.get_reachable_locations(state, self.player)
|
||||
if loc.address and not loc.item
|
||||
)
|
||||
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
|
||||
|
||||
needed_size = 3
|
||||
needed_size = 2
|
||||
needed_size += self.options.puzzle_randomization == "sigma_expert"
|
||||
needed_size += self.options.shuffle_symbols
|
||||
needed_size += self.options.shuffle_doors > 0
|
||||
@@ -247,9 +251,10 @@ class WitnessWorld(World):
|
||||
self.player_locations.add_location_late(loc)
|
||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
|
||||
|
||||
player = self.multiworld.get_player_name(self.player)
|
||||
|
||||
warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")
|
||||
warning(
|
||||
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
||||
due to insufficient sphere 1 size."""
|
||||
)
|
||||
|
||||
def create_items(self) -> None:
|
||||
# Determine pool size.
|
||||
@@ -286,7 +291,7 @@ class WitnessWorld(World):
|
||||
self.multiworld.push_precollected(self.create_item(inventory_item_name))
|
||||
|
||||
if len(item_pool) > pool_size:
|
||||
error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})"
|
||||
error(f"{self.player_name}'s Witness world has too few locations ({pool_size})"
|
||||
f" to place its necessary items ({len(item_pool)}).")
|
||||
return
|
||||
|
||||
@@ -296,7 +301,7 @@ class WitnessWorld(World):
|
||||
num_puzzle_skips = self.options.puzzle_skip_amount.value
|
||||
|
||||
if num_puzzle_skips > remaining_item_slots:
|
||||
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
|
||||
warning(f"{self.player_name}'s Witness world has insufficient locations"
|
||||
f" to place all requested puzzle skips.")
|
||||
num_puzzle_skips = remaining_item_slots
|
||||
item_pool["Puzzle Skip"] = num_puzzle_skips
|
||||
|
||||
@@ -104,6 +104,8 @@ GENERAL_LOCATIONS = {
|
||||
"Town RGB House Upstairs Right",
|
||||
"Town RGB House Sound Room Right",
|
||||
|
||||
"Town Pet the Dog",
|
||||
|
||||
"Windmill Theater Entry Panel",
|
||||
"Theater Exit Left Panel",
|
||||
"Theater Exit Right Panel",
|
||||
|
||||
@@ -147,6 +147,9 @@ class StaticWitnessLogicObj:
|
||||
elif "EP" in entity_name:
|
||||
entity_type = "EP"
|
||||
location_type = "EP"
|
||||
elif "Pet the Dog" in entity_name:
|
||||
entity_type = "Event"
|
||||
location_type = "Good Boi"
|
||||
elif entity_hex.startswith("0xFF"):
|
||||
entity_type = "Event"
|
||||
location_type = None
|
||||
|
||||
@@ -220,7 +220,7 @@ def try_getting_location_group_for_location(world: "WitnessWorld", hint_loc: Loc
|
||||
def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> WitnessWordedHint:
|
||||
location_name = hint.location.name
|
||||
if hint.location.player != world.player:
|
||||
location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"
|
||||
location_name += " (" + world.player_name + ")"
|
||||
|
||||
item = hint.location.item
|
||||
|
||||
@@ -229,7 +229,7 @@ def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint) -> Witnes
|
||||
item_name = item.name
|
||||
|
||||
if item.player != world.player:
|
||||
item_name += " (" + world.multiworld.get_player_name(item.player) + ")"
|
||||
item_name += " (" + world.player_name + ")"
|
||||
|
||||
hint_text = ""
|
||||
area: Optional[str] = None
|
||||
@@ -388,8 +388,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
|
||||
|
||||
while len(hints) < hint_amount:
|
||||
if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first:
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
logging.warning(f"Ran out of items/locations to hint for player {player_name}.")
|
||||
logging.warning(f"Ran out of items/locations to hint for player {world.player_name}.")
|
||||
break
|
||||
|
||||
location_hint: Optional[WitnessLocationHint]
|
||||
@@ -590,8 +589,7 @@ def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations
|
||||
hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount, hunt_panels))
|
||||
|
||||
if len(hinted_areas) < amount:
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
logging.warning(f"Was not able to make {amount} area hints for player {player_name}. "
|
||||
logging.warning(f"Was not able to make {amount} area hints for player {world.player_name}. "
|
||||
f"Made {len(hinted_areas)} instead, and filled the rest with random location hints.")
|
||||
|
||||
return hints, unhinted_locations_per_area
|
||||
@@ -680,8 +678,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
|
||||
|
||||
# If we still don't have enough for whatever reason, throw a warning, proceed with the lower amount
|
||||
if len(generated_hints) != hint_amount:
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
logging.warning(f"Couldn't generate {hint_amount} hints for player {player_name}. "
|
||||
logging.warning(f"Couldn't generate {hint_amount} hints for player {world.player_name}. "
|
||||
f"Generated {len(generated_hints)} instead.")
|
||||
|
||||
return generated_hints
|
||||
|
||||
@@ -19,7 +19,7 @@ class WitnessPlayerLocations:
|
||||
def __init__(self, world: "WitnessWorld", player_logic: WitnessPlayerLogic) -> None:
|
||||
"""Defines locations AFTER logic changes due to options"""
|
||||
|
||||
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"}
|
||||
self.PANEL_TYPES_TO_SHUFFLE = {"General", "Good Boi"}
|
||||
self.CHECK_LOCATIONS = static_witness_locations.GENERAL_LOCATIONS.copy()
|
||||
|
||||
if world.options.shuffle_discarded_panels:
|
||||
@@ -53,10 +53,6 @@ class WitnessPlayerLocations:
|
||||
if static_witness_logic.ENTITIES_BY_NAME[ch]["locationType"] in self.PANEL_TYPES_TO_SHUFFLE
|
||||
}
|
||||
|
||||
dog_hex = static_witness_logic.ENTITIES_BY_NAME["Town Pet the Dog"]["entity_hex"]
|
||||
dog_id = static_witness_locations.ALL_LOCATIONS_TO_ID["Town Pet the Dog"]
|
||||
self.CHECK_PANELHEX_TO_ID[dog_hex] = dog_id
|
||||
|
||||
self.CHECK_PANELHEX_TO_ID = dict(
|
||||
sorted(self.CHECK_PANELHEX_TO_ID.items(), key=lambda item: item[1])
|
||||
)
|
||||
|
||||
@@ -129,12 +129,18 @@ class ShuffleEnvironmentalPuzzles(Choice):
|
||||
option_obelisk_sides = 2
|
||||
|
||||
|
||||
class ShuffleDog(Toggle):
|
||||
class ShuffleDog(Choice):
|
||||
"""
|
||||
Adds petting the Town dog into the location pool.
|
||||
Adds petting the dog statue in Town into the location pool.
|
||||
Alternatively, you can force it to be a Puzzle Skip.
|
||||
"""
|
||||
display_name = "Pet the Dog"
|
||||
|
||||
option_off = 0
|
||||
option_puzzle_skip = 1
|
||||
option_random_item = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class EnvironmentalPuzzlesDifficulty(Choice):
|
||||
"""
|
||||
@@ -424,6 +430,7 @@ class TheWitnessOptions(PerGameCommonOptions):
|
||||
laser_hints: LaserHints
|
||||
death_link: DeathLink
|
||||
death_link_amnesty: DeathLinkAmnesty
|
||||
shuffle_dog: ShuffleDog
|
||||
|
||||
|
||||
witness_option_groups = [
|
||||
@@ -471,5 +478,8 @@ witness_option_groups = [
|
||||
ElevatorsComeToYou,
|
||||
DeathLink,
|
||||
DeathLinkAmnesty,
|
||||
]),
|
||||
OptionGroup("Silly Options", [
|
||||
ShuffleDog,
|
||||
])
|
||||
]
|
||||
|
||||
@@ -215,7 +215,7 @@ class WitnessPlayerItems:
|
||||
item = self.item_data[item_name]
|
||||
if isinstance(item.definition, ProgressiveItemDefinition):
|
||||
# Note: we need to reference the static table here rather than the player-specific one because the child
|
||||
# items were removed from the pool when we pruned out all progression items not in the settings.
|
||||
# items were removed from the pool when we pruned out all progression items not in the options.
|
||||
output[cast(int, item.ap_code)] = [cast(int, static_witness_items.ITEM_DATA[child_item].ap_code)
|
||||
for child_item in item.definition.child_item_names]
|
||||
return output
|
||||
|
||||
@@ -609,6 +609,9 @@ class WitnessPlayerLogic:
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
|
||||
if not world.options.shuffle_dog:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF80 (Town Pet the Dog)"])
|
||||
|
||||
if world.options.shuffle_boat:
|
||||
adjustment_linesets_in_order.append(get_boat())
|
||||
|
||||
@@ -771,8 +774,7 @@ class WitnessPlayerLogic:
|
||||
# If we are disabling a laser, something has gone wrong.
|
||||
if static_witness_logic.ENTITIES_BY_HEX[entity]["entityType"] == "Laser":
|
||||
laser_name = static_witness_logic.ENTITIES_BY_HEX[entity]["checkName"]
|
||||
player_name = world.multiworld.get_player_name(world.player)
|
||||
raise RuntimeError(f"Somehow, {laser_name} was disabled for player {player_name}."
|
||||
raise RuntimeError(f"Somehow, {laser_name} was disabled for player {world.player_name}."
|
||||
f" This is not allowed to happen, please report to Violet.")
|
||||
|
||||
newly_discovered_disabled_entities.add(entity)
|
||||
@@ -890,7 +892,7 @@ class WitnessPlayerLogic:
|
||||
)
|
||||
|
||||
def determine_unrequired_entities(self, world: "WitnessWorld") -> None:
|
||||
"""Figure out which major items are actually useless in this world's settings"""
|
||||
"""Figure out which major items are actually useless in this world's options"""
|
||||
|
||||
# Gather quick references to relevant options
|
||||
eps_shuffled = world.options.shuffle_EPs
|
||||
|
||||
@@ -37,6 +37,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"laser_hints": LaserHints.default,
|
||||
"death_link": DeathLink.default,
|
||||
"death_link_amnesty": DeathLinkAmnesty.default,
|
||||
|
||||
"shuffle_dog": ShuffleDog.default,
|
||||
},
|
||||
|
||||
# For relative beginners who want to move to the next step.
|
||||
@@ -73,6 +75,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"laser_hints": LaserHints.default,
|
||||
"death_link": DeathLink.default,
|
||||
"death_link_amnesty": DeathLinkAmnesty.default,
|
||||
|
||||
"shuffle_dog": ShuffleDog.default,
|
||||
},
|
||||
|
||||
# Allsanity but without the BS (no expert, no tedious EPs).
|
||||
@@ -109,5 +113,7 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"laser_hints": LaserHints.default,
|
||||
"death_link": DeathLink.default,
|
||||
"death_link_amnesty": DeathLinkAmnesty.default,
|
||||
|
||||
"shuffle_dog": ShuffleDog.option_random_item,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class TestExpertNonRandomizedEPs(WitnessTestBase):
|
||||
"victory_condition": "challenge",
|
||||
"shuffle_discarded_panels": False,
|
||||
"shuffle_boat": False,
|
||||
"shuffle_dog": "off",
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +25,7 @@ class TestVanillaAutoElevatorsPanels(WitnessTestBase):
|
||||
"early_caves": True,
|
||||
"shuffle_vault_boxes": True,
|
||||
"mountain_lasers": 11,
|
||||
"shuffle_dog": "puzzle_skip",
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +48,7 @@ class TestMaxEntityShuffle(WitnessTestBase):
|
||||
"obelisk_keys": True,
|
||||
"shuffle_lasers": "anywhere",
|
||||
"victory_condition": "mountain_box_long",
|
||||
"shuffle_dog": "random_item",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user