diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 88a0cec2ca..d1b672db7d 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,7 +1,8 @@ import logging from typing import Any, ClassVar, TextIO -from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial +from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, MultiWorld, Tutorial, \ + PlandoOptions from Options import Accessibility from Utils import output_path from settings import FilePath, Group @@ -18,6 +19,7 @@ from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules from .shop import FIGURINES, PROG_SHOP_ITEMS, SHOP_ITEMS, USEFUL_SHOP_ITEMS, shuffle_shop_prices from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation from .transitions import disconnect_entrances, shuffle_transitions +from .universal_tracker import reverse_portal_exits_into_portal_plando, reverse_transitions_into_plando_connections components.append( Component( @@ -151,6 +153,10 @@ class MessengerWorld(World): reachable_locs: bool = False filler: dict[str, int] + @staticmethod + def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]: + return slot_data + def generate_early(self) -> None: if self.options.goal == Goal.option_power_seal_hunt: self.total_seals = self.options.total_seals.value @@ -188,6 +194,11 @@ class MessengerWorld(World): self.spoiler_portal_mapping = {} self.transitions = [] + if hasattr(self.multiworld, "re_gen_passthrough"): + slot_data = self.multiworld.re_gen_passthrough.get(self.game) + if slot_data: + self.starting_portals = slot_data["starting_portals"] + def create_regions(self) -> None: # MessengerRegion adds itself to the multiworld # create simple regions @@ -279,6 +290,16 @@ class MessengerWorld(World): def connect_entrances(self) -> None: if self.options.shuffle_transitions: disconnect_entrances(self) + keep_entrance_logic = False + + if hasattr(self.multiworld, "re_gen_passthrough"): + slot_data = self.multiworld.re_gen_passthrough.get(self.game) + if slot_data: + self.multiworld.plando_options |= PlandoOptions.connections + self.options.portal_plando.value = reverse_portal_exits_into_portal_plando(slot_data["portal_exits"]) + self.options.plando_connections.value = reverse_transitions_into_plando_connections(slot_data["transitions"]) + keep_entrance_logic = True + add_closed_portal_reqs(self) # i need portal shuffle to happen after rules exist so i can validate it attempts = 20 @@ -295,7 +316,7 @@ class MessengerWorld(World): raise RuntimeError("Unable to generate valid portal output.") if self.options.shuffle_transitions: - shuffle_transitions(self) + shuffle_transitions(self, keep_entrance_logic) def write_spoiler_header(self, spoiler_handle: TextIO) -> None: if self.options.available_portals < 6: @@ -463,7 +484,7 @@ class MessengerWorld(World): "loc_data": {loc.address: {loc.item.name: [loc.item.code, loc.item.flags]} for loc in multiworld.get_filled_locations() if loc.address}, } - + output = orjson.dumps(data, option=orjson.OPT_NON_STR_KEYS) with open(out_path, "wb") as f: f.write(output) diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index 2e438fdbfd..8beed43027 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -1,8 +1,7 @@ from functools import cached_property from typing import TYPE_CHECKING -from BaseClasses import CollectionState, Entrance, EntranceType, Item, ItemClassification, Location, Region -from entrance_rando import ERPlacementState +from BaseClasses import CollectionState, Item, ItemClassification, Location, Region from .regions import LOCATIONS, MEGA_SHARDS from .shop import FIGURINES, SHOP_ITEMS diff --git a/worlds/messenger/transitions.py b/worlds/messenger/transitions.py index c0ae64c548..39ad591bf2 100644 --- a/worlds/messenger/transitions.py +++ b/worlds/messenger/transitions.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from BaseClasses import Entrance, Region +from BaseClasses import Region, CollectionRule from entrance_rando import EntranceType, randomize_entrances from .connections import RANDOMIZED_CONNECTIONS, TRANSITIONS from .options import ShuffleTransitions, TransitionPlando @@ -26,7 +26,6 @@ def disconnect_entrances(world: "MessengerWorld") -> None: entrance.randomization_type = er_type mock_entrance.randomization_type = er_type - for parent, child in RANDOMIZED_CONNECTIONS.items(): if child == "Corrupted Future": entrance = world.get_entrance("Artificer's Portal") @@ -36,8 +35,9 @@ def disconnect_entrances(world: "MessengerWorld") -> None: entrance = world.get_entrance(f"{parent} -> {child}") disconnect_entrance() -def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando) -> None: - def remove_dangling_exit(region: Region) -> None: + +def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando, keep_logic: bool = False) -> None: + def remove_dangling_exit(region: Region) -> CollectionRule: # find the disconnected exit and remove references to it for _exit in region.exits: if not _exit.connected_region: @@ -45,6 +45,7 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando else: raise ValueError(f"Unable to find randomized transition for {plando_connection}") region.exits.remove(_exit) + return _exit.access_rule def remove_dangling_entrance(region: Region) -> None: # find the disconnected entrance and remove references to it @@ -65,30 +66,35 @@ def connect_plando(world: "MessengerWorld", plando_connections: TransitionPlando else: dangling_exit = world.get_entrance("Artificer's Challenge") reg1.exits.remove(dangling_exit) + access_rule = dangling_exit.access_rule else: reg1 = world.get_region(plando_connection.entrance) - remove_dangling_exit(reg1) - + access_rule = remove_dangling_exit(reg1) + reg2 = world.get_region(plando_connection.exit) remove_dangling_entrance(reg2) # connect the regions - reg1.connect(reg2) + new_exit1 = reg1.connect(reg2) + if keep_logic: + new_exit1.access_rule = access_rule # pretend the user set the plando direction as "both" regardless of what they actually put on coupled if ((world.options.shuffle_transitions == ShuffleTransitions.option_coupled or plando_connection.direction == "both") and plando_connection.exit in RANDOMIZED_CONNECTIONS): - remove_dangling_exit(reg2) + access_rule = remove_dangling_exit(reg2) remove_dangling_entrance(reg1) - reg2.connect(reg1) + new_exit2 = reg2.connect(reg1) + if keep_logic: + new_exit2.access_rule = access_rule -def shuffle_transitions(world: "MessengerWorld") -> None: +def shuffle_transitions(world: "MessengerWorld", keep_logic: bool = False) -> None: coupled = world.options.shuffle_transitions == ShuffleTransitions.option_coupled plando = world.options.plando_connections if plando: - connect_plando(world, plando) + connect_plando(world, plando, keep_logic) result = randomize_entrances(world, coupled, {0: [0]}) diff --git a/worlds/messenger/universal_tracker.py b/worlds/messenger/universal_tracker.py new file mode 100644 index 0000000000..9d752031bf --- /dev/null +++ b/worlds/messenger/universal_tracker.py @@ -0,0 +1,41 @@ +from Options import PlandoConnection +from .connections import RANDOMIZED_CONNECTIONS +from .portals import REGION_ORDER, SHOP_POINTS, CHECKPOINTS +from .transitions import TRANSITIONS + +REVERSED_RANDOMIZED_CONNECTIONS = {v: k for k, v in RANDOMIZED_CONNECTIONS.items()} + + +def find_spot(portal_key: int) -> str: + """finds the spot associated with the portal key""" + parent = REGION_ORDER[portal_key // 100] + if portal_key % 100 == 0: + return f"{parent} Portal" + if portal_key % 100 // 10 == 1: + return SHOP_POINTS[parent][portal_key % 10] + return CHECKPOINTS[parent][portal_key % 10] + + +def reverse_portal_exits_into_portal_plando(portal_exits: list[int]) -> list[PlandoConnection]: + return [ + PlandoConnection("Autumn Hills", find_spot(portal_exits[0]), "both"), + PlandoConnection("Riviere Turquoise", find_spot(portal_exits[1]), "both"), + PlandoConnection("Howling Grotto", find_spot(portal_exits[2]), "both"), + PlandoConnection("Sunken Shrine", find_spot(portal_exits[3]), "both"), + PlandoConnection("Searing Crags", find_spot(portal_exits[4]), "both"), + PlandoConnection("Glacial Peak", find_spot(portal_exits[5]), "both"), + ] + + +def reverse_transitions_into_plando_connections(transitions: list[list[int]]) -> list[PlandoConnection]: + plando_connections = [] + + for connection in [ + PlandoConnection(REVERSED_RANDOMIZED_CONNECTIONS[TRANSITIONS[transition[0]]], TRANSITIONS[transition[1]], "both") + for transition in transitions + ]: + if connection.exit in {con.entrance for con in plando_connections}: + continue + plando_connections.append(connection) + + return plando_connections