mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-27 03:39:56 -07:00
Merge branch 'main' into civ6-1.0
This commit is contained in:
@@ -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": ""
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+20
-15
@@ -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