From 424f5d4a830fb43f86c76d73d795412890d55bc2 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 22 Sep 2025 12:05:18 -0400 Subject: [Apworld] Added worldport shuffle --- apworld/__init__.py | 29 ++++++++++++++++++-- apworld/options.py | 11 ++++++++ apworld/player_logic.py | 4 +-- apworld/regions.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ apworld/rules.py | 7 +++-- 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/apworld/__init__.py b/apworld/__init__.py index f1de503..2213e33 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -6,7 +6,7 @@ from worlds.AutoWorld import WebWorld, World from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS from .options import Lingo2Options from .player_logic import Lingo2PlayerLogic -from .regions import create_regions +from .regions import create_regions, shuffle_entrances, connect_ports_from_ut from .static_logic import Lingo2StaticLogic from .version import APWORLD_VERSION @@ -46,12 +46,25 @@ class Lingo2World(World): player_logic: Lingo2PlayerLogic + port_pairings: dict[int, int] + def generate_early(self): self.player_logic = Lingo2PlayerLogic(self) + self.port_pairings = {} def create_regions(self): create_regions(self) + def connect_entrances(self): + if self.options.shuffle_worldports: + if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: + slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] + self.port_pairings = {int(fp): int(tp) for fp, tp in slot_value.items()} + + connect_ports_from_ut(self.port_pairings, self) + else: + shuffle_entrances(self) + from Utils import visualize_regions visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") @@ -100,17 +113,29 @@ class Lingo2World(World): "shuffle_gallery_paintings", "shuffle_letters", "shuffle_symbols", + "shuffle_worldports", "strict_cyan_ending", "strict_purple_ending", "victory_condition", ] - slot_data = { + slot_data: dict[str, object] = { **self.options.as_dict(*slot_options), "version": [self.static_logic.get_data_version(), APWORLD_VERSION], } + if self.options.shuffle_worldports: + slot_data["port_pairings"] = self.port_pairings + return slot_data def get_filler_item_name(self) -> str: return "A Job Well Done" + + # for the universal tracker, doesn't get called in standard gen + # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md + @staticmethod + def interpret_slot_data(slot_data: dict[str, object]) -> dict[str, object]: + # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough + # we are using re_gen_passthrough over modifying the world here due to complexities with ER + return slot_data diff --git a/apworld/options.py b/apworld/options.py index 3646eea..795010a 100644 --- a/apworld/options.py +++ b/apworld/options.py @@ -52,6 +52,16 @@ class ShuffleSymbols(Toggle): display_name = "Shuffle Symbols" +class ShuffleWorldports(Toggle): + """ + Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in + order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections + like the one between The Shop and Control Center. Connections that depend on placing letters in keyholders are also + currently not shuffled. + """ + display_name = "Shuffle Worldports" + + class KeyholderSanity(Toggle): """ If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder. @@ -157,6 +167,7 @@ class Lingo2Options(PerGameCommonOptions): shuffle_gallery_paintings: ShuffleGalleryPaintings shuffle_letters: ShuffleLetters shuffle_symbols: ShuffleSymbols + shuffle_worldports: ShuffleWorldports keyholder_sanity: KeyholderSanity cyan_door_behavior: CyanDoorBehavior daedalus_roof_access: DaedalusRoofAccess diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 4aa481d..966f712 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -234,10 +234,10 @@ class Lingo2PlayerLogic: for door_group in world.static_logic.objects.door_groups: if door_group.type == data_pb2.DoorGroupType.CONNECTOR: - if not self.world.options.shuffle_doors: + if not self.world.options.shuffle_doors or self.world.options.shuffle_worldports: continue elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR: - if not self.world.options.shuffle_control_center_colors: + if not self.world.options.shuffle_control_center_colors or self.world.options.shuffle_worldports: continue elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP: if not self.world.options.shuffle_doors: diff --git a/apworld/regions.py b/apworld/regions.py index 993eec8..a7d9a1c 100644 --- a/apworld/regions.py +++ b/apworld/regions.py @@ -1,6 +1,8 @@ from typing import TYPE_CHECKING +import BaseClasses from BaseClasses import Region, ItemClassification, Entrance +from entrance_rando import randomize_entrances from .items import Lingo2Item from .locations import Lingo2Location from .player_logic import AccessRequirements @@ -76,6 +78,9 @@ def create_regions(world: "Lingo2World"): port = world.static_logic.objects.ports[connection.port] connection_name = f"{connection_name} (via port {port.name})" + if world.options.shuffle_worldports and not port.no_shuffle: + continue + if port.HasField("required_door"): reqs.merge(world.player_logic.get_door_open_reqs(port.required_door)) @@ -116,3 +121,68 @@ def create_regions(world: "Lingo2World"): world.multiworld.register_indirect_condition(regions[region], connection) world.multiworld.regions += regions.values() + + +def shuffle_entrances(world: "Lingo2World"): + er_entrances: list[Entrance] = [] + er_exits: list[Entrance] = [] + + port_id_by_name: dict[str, int] = {} + + for port in world.static_logic.objects.ports: + if port.no_shuffle: + continue + + port_region_name = world.static_logic.get_room_region_name(port.room_id) + port_region = world.multiworld.get_region(port_region_name, world.player) + + connection_name = f"{port_region_name} - {port.name}" + port_id_by_name[connection_name] = port.id + + entrance = port_region.create_er_target(connection_name) + entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY + + er_exit = port_region.create_exit(connection_name) + er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY + + if port.HasField("required_door"): + door_reqs = world.player_logic.get_door_open_reqs(port.required_door) + er_exit.access_rule = make_location_lambda(door_reqs, world, None) + + for region in door_reqs.get_referenced_rooms(): + world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), + er_exit) + + er_entrances.append(entrance) + er_exits.append(er_exit) + + result = randomize_entrances(world, True, {0:[0]}, False, er_entrances, + er_exits) + + for (f, to) in result.pairings: + world.port_pairings[port_id_by_name[f]] = port_id_by_name[to] + + +def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"): + for fpid, tpid in port_pairings.items(): + from_port = world.static_logic.objects.ports[fpid] + to_port = world.static_logic.objects.ports[tpid] + + from_region_name = world.static_logic.get_room_region_name(from_port.room_id) + to_region_name = world.static_logic.get_room_region_name(to_port.room_id) + + from_region = world.multiworld.get_region(from_region_name, world.player) + to_region = world.multiworld.get_region(to_region_name, world.player) + + connection = Entrance(world.player, f"{from_region_name} - {from_port.name}", from_region) + + if from_port.HasField("required_door"): + door_reqs = world.player_logic.get_door_open_reqs(from_port.required_door) + connection.access_rule = make_location_lambda(door_reqs, world, None) + + for region in door_reqs.get_referenced_rooms(): + world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), + connection) + + from_region.exits.append(connection) + connection.connect(to_region) diff --git a/apworld/rules.py b/apworld/rules.py index c077858..f859e75 100644 --- a/apworld/rules.py +++ b/apworld/rules.py @@ -54,10 +54,13 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem return True def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", - regions: dict[str, Region]) -> Callable[[CollectionState], bool]: + regions: dict[str, Region] | None) -> Callable[[CollectionState], bool]: # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule # checking. - required_regions = [regions[room_name] for room_name in reqs.rooms] + if regions is not None: + required_regions = [regions[room_name] for room_name in reqs.rooms] + else: + required_regions = [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms] new_reqs = reqs.copy() new_reqs.rooms.clear() return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world) -- cgit 1.4.1