Merge branch 'main' into civ6-1.0

This commit is contained in:
Carter Hesterman
2024-08-23 20:51:08 -06:00
committed by GitHub
16 changed files with 139 additions and 83 deletions

View File

@@ -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))

View File

@@ -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

View File

@@ -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()

View File

@@ -28,7 +28,7 @@ An Example `AP.json` file:
```
{
"Url": "archipelago:12345",
"Url": "archipelago.gg:12345",
"SlotName": "Maddy",
"Password": ""
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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])
)

View File

@@ -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,
])
]

View File

@@ -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

View File

@@ -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

View File

@@ -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,
},
}

View File

@@ -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",
}