From c2be561667e666a02a6d5716071436593879eb40 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 24 Nov 2023 20:13:33 -0600 Subject: [PATCH 01/38] initial commit, works but need to port games --- Generate.py | 23 ------ Options.py | 142 ++++++++++++++++++++++++++++++++++++- worlds/generic/__init__.py | 8 +-- 3 files changed, 140 insertions(+), 33 deletions(-) diff --git a/Generate.py b/Generate.py index 8113d8a0d7da..0cb22053ce22 100644 --- a/Generate.py +++ b/Generate.py @@ -474,18 +474,6 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b handle_option(ret, game_weights, option_key, option, plando_options) if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) - if ret.game == "Minecraft" or ret.game == "Ocarina of Time": - # bad hardcoded behavior to make this work for now - ret.plando_connections = [] - if PlandoOptions.connections in plando_options: - options = game_weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice("entrance", placement), - get_choice("exit", placement), - get_choice("direction", placement) - )) elif ret.game == "A Link to the Past": roll_alttp_settings(ret, game_weights, plando_options) @@ -601,17 +589,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): raise Exception(f"No text target \"{at}\" found.") ret.plando_texts[at] = str(get_choice_legacy("text", placement)) - ret.plando_connections = [] - if PlandoOptions.connections in plando_options: - options = weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice_legacy("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice_legacy("entrance", placement), - get_choice_legacy("exit", placement), - get_choice_legacy("direction", placement, "both") - )) - ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice_legacy('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/Options.py b/Options.py index 9b4f9d990879..4369754407c3 100644 --- a/Options.py +++ b/Options.py @@ -59,6 +59,7 @@ def __new__(mcs, name, bases, attrs): def verify(self, *args, **kwargs) -> None: for f in verifiers: f(self, *args, **kwargs) + attrs["verify"] = verify else: assert verifiers, "class Option is supposed to implement def verify" @@ -183,6 +184,7 @@ def get_option_name(cls, value: str) -> str: class NumericOption(Option[int], numbers.Integral, abc.ABC): default = 0 + # note: some of the `typing.Any`` here is a result of unresolved issue in python standards # `int` is not a `numbers.Integral` according to the official typestubs # (even though isinstance(5, numbers.Integral) == True) @@ -598,7 +600,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P if isinstance(self.value, int): return from BaseClasses import PlandoOptions - if not(PlandoOptions.bosses & plando_options): + if not (PlandoOptions.bosses & plando_options): # plando is disabled but plando options were given so pull the option and change it to an int option = self.value.split(";")[-1] self.value = self.options[option] @@ -878,6 +880,139 @@ class ItemSet(OptionSet): convert_name_groups = True +class ConnectionsMeta(AssembleOptions): + def __new__(mcs, name, bases, attrs): + if name != "PlandoConnections": + if attrs["shared_connections"]: + assert "connections" in attrs, f"Please define valid connections for {name}" + attrs["connections"] = frozenset((connection.lower() for connection in attrs["connections"])) + else: + assert "entrances" in attrs, f"Please define valid entrances for {name}" + attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) + assert "exits" in attrs, f"Please define valid exits for {name}" + attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"])) + cls = super().__new__(mcs, name, bases, attrs) + return cls + + +class PlandoConnection(typing.NamedTuple): + entrance: str + exit: str + direction: str # entrance, exit or both + + +class PlandoConnections(Option[typing.List["PlandoConnection"]], metaclass=ConnectionsMeta): + """Generic connections plando. Format is: + - entrance: "Entrance Name" + exit: "Exit Name" + direction: "Direction" + Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both'. + Must override can_connect, which passes an entrance and an exit. Check to see if the connection is valid.""" + class Direction: + Entrance = "entrance" + Exit = "exit" + Both = "both" + + display_name = "Plando Connections" + shared_connections: bool = False + """True if all connections can be an entrance or an exit, False otherwise.""" + + default: typing.List[PlandoConnection] = [] + supports_weighting = False + + entrances: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + exits: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + connections: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + + def __init__(self, value: typing.List[PlandoConnection]): + self.value = deepcopy(value) + super(PlandoConnections, self).__init__() + + @classmethod + def validate_entrance_name(cls, entrance): + if cls.shared_connections: + return entrance.lower() in cls.connections + else: + return entrance.lower() in cls.entrances + + @classmethod + def validate_exit_name(cls, exit): + if cls.shared_connections: + return exit.lower() in cls.connections + else: + return exit.lower() in cls.exits + + @classmethod + def can_connect(cls, entrance, exit): + raise NotImplementedError + + @classmethod + def validate_plando_connections(cls, connections): + used_entrances = [] + used_exits = [] + for connection in connections: + entrance = connection.entrance + exit = connection.exit + if entrance in used_entrances: + raise ValueError(f"Duplicate Entrance {entrance} not allowed.") + if exit in used_exits: + raise ValueError(f"Duplicate Exit {exit} not allowed.") + used_entrances.append(entrance) + used_exits.append(exit) + if not cls.validate_entrance_name(entrance): + raise ValueError(f"{entrance.title()} is not a valid entrance.") + if not cls.validate_exit_name(exit): + raise ValueError(f"{exit.title()} is not a valid exit.") + if not cls.can_connect(entrance, exit): + raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + + @classmethod + def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoConnection]]: + if type(data) == list: + value = [] + for connection in data: + if type(connection) == dict: + entrance = connection.get("entrance", None) + exit = connection.get("exit", None) + direction = connection.get("direction", "both") + if not entrance or not exit: + raise Exception("Plando connection must have an entrance and an exit.") + value.append(PlandoConnection( + entrance, + exit, + direction + )) + elif type(connection) == PlandoConnection: + value.append(connection) + else: + raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") + cls.validate_plando_connections(value) + return cls(value) + else: + raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.") + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if not (PlandoOptions.connections & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando connections module is turned off, " + f"so connections for {player_name} will be ignored.") + + def get_option_name(self, value): + return ", ".join(["%s %s %s" % (connection.entrance, + "<=>" if connection.direction == self.Direction.Both else + "<=" if connection.direction == self.Direction.Exit else + "=>", + connection.exit) for connection in value]) + + def __iter__(self): + yield from self.value + + def __len__(self): + return len(self.value) + + class Accessibility(Choice): """Set rules for reachability of your items/locations. Locations: ensure everything can be reached and acquired. @@ -911,7 +1046,7 @@ def __new__(mcs, bases: typing.Tuple[type, ...], attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty": for attr_type in attrs.values(): - assert not isinstance(attr_type, AssembleOptions),\ + assert not isinstance(attr_type, AssembleOptions), \ f"Options for {name} should be type hinted on the class, not assigned" return super().__new__(mcs, name, bases, attrs) @@ -1029,7 +1164,8 @@ class ItemLinks(OptionList): ]) @staticmethod - def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set: + def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, + allow_item_groups: bool = True) -> typing.Set: pool = set() for item_name in items: if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups): diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 6b2ffdfee180..00b1f6572415 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -5,7 +5,7 @@ from ..AutoWorld import World, WebWorld from NetUtils import SlotType - +from Options import PlandoConnection class GenericWeb(WebWorld): advanced_settings = Tutorial('Advanced YAML Guide', @@ -69,9 +69,3 @@ def failed(self, warning: str, exception=Exception): raise exception(warning) else: self.warn(warning) - - -class PlandoConnection(NamedTuple): - entrance: str - exit: str - direction: str # entrance, exit or both From 9ec268db2bc63427c95893060487102bc3fc63e6 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 3 Jan 2024 00:47:29 -0600 Subject: [PATCH 02/38] port lttp and minecraft --- worlds/alttp/Options.py | 19 ++++++++++++++++--- worlds/minecraft/Options.py | 9 ++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index a89a9adb83a7..50e240b57966 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,7 +1,10 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ + StartInventoryPool, PlandoBosses, PlandoConnections +from .EntranceShuffle import default_connections, default_dungeon_connections, \ + inverted_default_connections, inverted_default_dungeon_connections class Logic(Choice): @@ -47,8 +50,8 @@ def to_bool(self, world: MultiWorld, player: int) -> bool: return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} elif self.value == self.option_auto: return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \ - and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not - world.shuffle_ganon) + and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not + world.shuffle_ganon) elif self.value == self.option_open: return True else: @@ -432,7 +435,17 @@ class AllowCollect(Toggle): display_name = "Allow Collection of checks for other players" +class ALttPPlandoConnections(PlandoConnections): + entrances = set([connection[0] for connection in ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + exits = set([connection[1] for connection in ( + *default_connections, *default_dungeon_connections, *inverted_default_connections, + *inverted_default_dungeon_connections)]) + + alttp_options: typing.Dict[str, type(Option)] = { + "plando_connections": ALttPPlandoConnections, "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, "open_pyramid": OpenPyramid, diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index cdb5bf303f47..1abb40753627 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink +from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections +from .Constants import region_info class AdvancementGoal(Range): @@ -97,7 +98,13 @@ class StartingItems(OptionList): display_name = "Starting Items" +class MCPlandoConnections(PlandoConnections): + entrances = set(connection[0] for connection in region_info["default_connections"]) + exits = set(connection[1] for connection in region_info["default_connections"]) + + minecraft_options: typing.Dict[str, type(Option)] = { + "plando_connections": MCPlandoConnections, "advancement_goal": AdvancementGoal, "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, From 226932e59514e03684d26e22de24fcb3c0cd9df8 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 3 Jan 2024 02:45:29 -0600 Subject: [PATCH 03/38] rewrite can_connect to return true by default --- Options.py | 4 +++- worlds/alttp/Options.py | 1 + worlds/minecraft/Options.py | 7 +++++++ worlds/oot/Options.py | 10 +++++++++- worlds/oot/__init__.py | 4 +++- 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index 6c43fc247228..9ec2efcc8e66 100644 --- a/Options.py +++ b/Options.py @@ -965,7 +965,9 @@ def validate_exit_name(cls, exit): @classmethod def can_connect(cls, entrance, exit): - raise NotImplementedError + """Checks that a given entrance can connect to a given exit. + By default, this will always return true unless overridden.""" + return True @classmethod def validate_plando_connections(cls, connections): diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 50e240b57966..18b185f54dbe 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -442,6 +442,7 @@ class ALttPPlandoConnections(PlandoConnections): exits = set([connection[1] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) + shared_connections = False alttp_options: typing.Dict[str, type(Option)] = { diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 1abb40753627..175fa0116583 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -101,6 +101,13 @@ class StartingItems(OptionList): class MCPlandoConnections(PlandoConnections): entrances = set(connection[0] for connection in region_info["default_connections"]) exits = set(connection[1] for connection in region_info["default_connections"]) + shared_connections = False + + @classmethod + def can_connect(cls, entrance, exit): + if exit in region_info["illegal_connections"] and exit in region_info["illegal_connections"][exit]: + return False + return True minecraft_options: typing.Dict[str, type(Option)] = { diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 120027e29dfa..86a1cbb07a65 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1,6 +1,7 @@ import typing import random -from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink +from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections +from .EntranceShuffle import entrance_shuffle_table from .LogicTricks import normalized_name_tricks from .ColorSFXOptions import * @@ -29,6 +30,12 @@ def from_any(cls, data: typing.Any) -> Range: raise RuntimeError(f"All options specified in \"{cls.display_name}\" are weighted as zero.") +class OoTPlandoConnections(PlandoConnections): + shared_connections = False + entrances = set([connection[1][0] for connection in entrance_shuffle_table]) + exits = set([connection[2][0] for connection in entrance_shuffle_table if len(connection) > 2]) + + class Logic(Choice): """Set the logic used for the generator. Glitchless: Normal gameplay. Can enable more difficult logical paths using the Logic Tricks option. @@ -1277,6 +1284,7 @@ class LogicTricks(OptionList): # All options assembled into a single dict oot_options: typing.Dict[str, type(Option)] = { + "plando_connections": OoTPlandoConnections, "logic_rules": Logic, "logic_no_night_tokens_without_suns_song": NightTokens, **open_options, diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index e9c889d6f653..2ec8d59741ac 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -32,7 +32,7 @@ from Utils import get_options from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType -from Options import Range, Toggle, VerifyKeys, Accessibility +from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections from Fill import fill_restrictive, fast_fill, FillError from worlds.generic.Rules import exclusion_rules, add_item_rule from ..AutoWorld import World, AutoLogicRegister, WebWorld @@ -194,6 +194,8 @@ def generate_early(self): option_value = bool(result) elif isinstance(result, VerifyKeys): option_value = result.value + elif isinstance(result, PlandoConnections): + option_value = result.value else: option_value = result.current_key setattr(self, option_name, option_value) From 1f43d4d62e1c9e37d4f322b19d49b9117f7e3ff0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 4 Feb 2024 02:08:55 -0600 Subject: [PATCH 04/38] text support + update to always use entrance/exit --- Generate.py | 12 -- Options.py | 96 ++++++--- worlds/alttp/Options.py | 14 +- worlds/alttp/Text.py | 409 ++++++++++++++++++++++++++++++++++++ worlds/minecraft/Options.py | 1 - worlds/oot/Options.py | 1 - 6 files changed, 484 insertions(+), 49 deletions(-) diff --git a/Generate.py b/Generate.py index 9994845428bb..84d01852851b 100644 --- a/Generate.py +++ b/Generate.py @@ -588,18 +588,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): if not ret.required_medallions[index]: raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}") - ret.plando_texts = {} - if PlandoOptions.texts in plando_options: - tt = TextTable() - tt.removeUnwantedText() - options = weights.get("plando_texts", []) - for placement in options: - if roll_percentage(get_choice_legacy("percentage", placement, 100)): - at = str(get_choice_legacy("at", placement)) - if at not in tt: - raise Exception(f"No text target \"{at}\" found.") - ret.plando_texts[at] = str(get_choice_legacy("text", placement)) - ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice_legacy('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/Options.py b/Options.py index 9ec2efcc8e66..e5e81ac5d842 100644 --- a/Options.py +++ b/Options.py @@ -901,17 +901,49 @@ class ItemSet(OptionSet): convert_name_groups = True +class PlandoTexts(Option[typing.Dict[str, str]], VerifyKeys): + default: typing.List = [] + supports_weighting = False + + def __init__(self, value: typing.Dict[str, str]): + self.value = deepcopy(value) + super().__init__() + + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: + from BaseClasses import PlandoOptions + if not (PlandoOptions.texts & plando_options): + # plando is disabled but plando options were given so overwrite the options + self.value = [] + logging.warning(f"The plando texts module is turned off, " + f"so text for {player_name} will be ignored.") + + @classmethod + def from_any(cls, data: typing.List[typing.Any]) -> Option[typing.Dict[str, str]]: + texts = {} + if type(data) == list: + for text in data: + if type(text) == dict: + if random.random() < float(text.get("percentage", 100)/100): + at = text.get("at", None) + if at is not None: + texts[at] = text.get("text", "") + else: + raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") + cls.verify_keys([text.at for text in texts]) + return cls(texts) + else: + raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") + + class ConnectionsMeta(AssembleOptions): def __new__(mcs, name, bases, attrs): if name != "PlandoConnections": - if attrs["shared_connections"]: - assert "connections" in attrs, f"Please define valid connections for {name}" - attrs["connections"] = frozenset((connection.lower() for connection in attrs["connections"])) - else: - assert "entrances" in attrs, f"Please define valid entrances for {name}" - attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) - assert "exits" in attrs, f"Please define valid exits for {name}" - attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"])) + assert "entrances" in attrs, f"Please define valid entrances for {name}" + attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) + assert "exits" in attrs, f"Please define valid exits for {name}" + attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"])) + if "__doc__" not in attrs: + attrs["__doc__"] = PlandoConnections.__doc__ cls = super().__new__(mcs, name, bases, attrs) return cls @@ -920,30 +952,29 @@ class PlandoConnection(typing.NamedTuple): entrance: str exit: str direction: str # entrance, exit or both + percentage: int = 100 -class PlandoConnections(Option[typing.List["PlandoConnection"]], metaclass=ConnectionsMeta): +class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta): """Generic connections plando. Format is: - entrance: "Entrance Name" exit: "Exit Name" direction: "Direction" - Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both'. - Must override can_connect, which passes an entrance and an exit. Check to see if the connection is valid.""" + percentage: 100 + Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" class Direction: Entrance = "entrance" Exit = "exit" Both = "both" display_name = "Plando Connections" - shared_connections: bool = False - """True if all connections can be an entrance or an exit, False otherwise.""" default: typing.List[PlandoConnection] = [] supports_weighting = False entrances: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] exits: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] - connections: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] def __init__(self, value: typing.List[PlandoConnection]): self.value = deepcopy(value) @@ -951,17 +982,11 @@ def __init__(self, value: typing.List[PlandoConnection]): @classmethod def validate_entrance_name(cls, entrance): - if cls.shared_connections: - return entrance.lower() in cls.connections - else: - return entrance.lower() in cls.entrances + return entrance.lower() in cls.entrances @classmethod def validate_exit_name(cls, exit): - if cls.shared_connections: - return exit.lower() in cls.connections - else: - return exit.lower() in cls.exits + return exit.lower() in cls.exits @classmethod def can_connect(cls, entrance, exit): @@ -995,18 +1020,23 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoConnection]]: value = [] for connection in data: if type(connection) == dict: - entrance = connection.get("entrance", None) - exit = connection.get("exit", None) - direction = connection.get("direction", "both") - if not entrance or not exit: - raise Exception("Plando connection must have an entrance and an exit.") - value.append(PlandoConnection( - entrance, - exit, - direction - )) + percentage = connection.get("percentage", 100) + if random.random() < float(percentage / 100): + entrance = connection.get("entrance", None) + exit = connection.get("exit", None) + direction = connection.get("direction", "both") + + if not entrance or not exit: + raise Exception("Plando connection must have an entrance and an exit.") + value.append(PlandoConnection( + entrance, + exit, + direction, + percentage + )) elif type(connection) == PlandoConnection: - value.append(connection) + if random.random() < float(connection.percentage / 100): + value.append(connection) else: raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") cls.validate_plando_connections(value) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 18b185f54dbe..49993b444f17 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -2,9 +2,10 @@ from BaseClasses import MultiWorld from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \ - StartInventoryPool, PlandoBosses, PlandoConnections + StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts from .EntranceShuffle import default_connections, default_dungeon_connections, \ inverted_default_connections, inverted_default_dungeon_connections +from .Text import TextTable class Logic(Choice): @@ -442,11 +443,20 @@ class ALttPPlandoConnections(PlandoConnections): exits = set([connection[1] for connection in ( *default_connections, *default_dungeon_connections, *inverted_default_connections, *inverted_default_dungeon_connections)]) - shared_connections = False + + +class ALttPPlandoTexts(PlandoTexts): + """Text plando. Format is: + - text: 'This is your text' + at: text_key + percentage: 100 + Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" + valid_keys = TextTable.valid_keys alttp_options: typing.Dict[str, type(Option)] = { "plando_connections": ALttPPlandoConnections, + "plando_texts": ALttPPlandoTexts, "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, "open_pyramid": OpenPyramid, diff --git a/worlds/alttp/Text.py b/worlds/alttp/Text.py index b479a9b8e002..c005cacd8f9f 100644 --- a/worlds/alttp/Text.py +++ b/worlds/alttp/Text.py @@ -1289,6 +1289,415 @@ class LargeCreditBottomMapper(CharTextMapper): class TextTable(object): SIZE = 0x7355 + valid_keys = [ + "set_cursor", + "set_cursor2", + "game_over_menu", + "var_test", + "follower_no_enter", + "choice_1_3", + "choice_2_3", + "choice_3_3", + "choice_1_2", + "choice_2_2", + "uncle_leaving_text", + "uncle_dying_sewer", + "tutorial_guard_1", + "tutorial_guard_2", + "tutorial_guard_3", + "tutorial_guard_4", + "tutorial_guard_5", + "tutorial_guard_6", + "tutorial_guard_7", + "priest_sanctuary_before_leave", + "sanctuary_enter", + "zelda_sanctuary_story", + "priest_sanctuary_before_pendants", + "priest_sanctuary_after_pendants_before_master_sword", + "priest_sanctuary_dying", + "zelda_save_sewers", + "priest_info", + "zelda_sanctuary_before_leave", + "telepathic_intro", + "telepathic_reminder", + "zelda_go_to_throne", + "zelda_push_throne", + "zelda_switch_room_pull", + "zelda_save_lets_go", + "zelda_save_repeat", + "zelda_before_pendants", + "zelda_after_pendants_before_master_sword", + "telepathic_zelda_right_after_master_sword", + "zelda_sewers", + "zelda_switch_room", + "kakariko_saharalasa_wife", + "kakariko_saharalasa_wife_sword_story", + "kakariko_saharalasa_wife_closing", + "kakariko_saharalasa_after_master_sword", + "kakariko_alert_guards", + "sahasrahla_quest_have_pendants", + "sahasrahla_quest_have_master_sword", + "sahasrahla_quest_information", + "sahasrahla_bring_courage", + "sahasrahla_have_ice_rod", + "telepathic_sahasrahla_beat_agahnim", + "telepathic_sahasrahla_beat_agahnim_no_pearl", + "sahasrahla_have_boots_no_icerod", + "sahasrahla_have_courage", + "sahasrahla_found", + "sign_rain_north_of_links_house", + "sign_north_of_links_house", + "sign_path_to_death_mountain", + "sign_lost_woods", + "sign_zoras", + "sign_outside_magic_shop", + "sign_death_mountain_cave_back", + "sign_east_of_links_house", + "sign_south_of_lumberjacks", + "sign_east_of_desert", + "sign_east_of_sanctuary", + "sign_east_of_castle", + "sign_north_of_lake", + "sign_desert_thief", + "sign_lumberjacks_house", + "sign_north_kakariko", + "witch_bring_mushroom", + "witch_brewing_the_item", + "witch_assistant_no_bottle", + "witch_assistant_no_empty_bottle", + "witch_assistant_informational", + "witch_assistant_no_bottle_buying", + "potion_shop_no_empty_bottles", + "item_get_lamp", + "item_get_boomerang", + "item_get_bow", + "item_get_shovel", + "item_get_magic_cape", + "item_get_powder", + "item_get_flippers", + "item_get_power_gloves", + "item_get_pendant_courage", + "item_get_pendant_power", + "item_get_pendant_wisdom", + "item_get_mushroom", + "item_get_book", + "item_get_moonpearl", + "item_get_compass", + "item_get_map", + "item_get_ice_rod", + "item_get_fire_rod", + "item_get_ether", + "item_get_bombos", + "item_get_quake", + "item_get_hammer", + "item_get_flute", + "item_get_cane_of_somaria", + "item_get_hookshot", + "item_get_bombs", + "item_get_bottle", + "item_get_big_key", + "item_get_titans_mitts", + "item_get_magic_mirror", + "item_get_fake_mastersword", + "post_item_get_mastersword", + "item_get_red_potion", + "item_get_green_potion", + "item_get_blue_potion", + "item_get_bug_net", + "item_get_blue_mail", + "item_get_red_mail", + "item_get_temperedsword", + "item_get_mirror_shield", + "item_get_cane_of_byrna", + "missing_big_key", + "missing_magic", + "item_get_pegasus_boots", + "talking_tree_info_start", + "talking_tree_info_1", + "talking_tree_info_2", + "talking_tree_info_3", + "talking_tree_info_4", + "talking_tree_other", + "item_get_pendant_power_alt", + "item_get_pendant_wisdom_alt", + "game_shooting_choice", + "game_shooting_yes", + "game_shooting_no", + "game_shooting_continue", + "pond_of_wishing", + "pond_item_select", + "pond_item_test", + "pond_will_upgrade", + "pond_item_test_no", + "pond_item_test_no_no", + "pond_item_boomerang", + "pond_item_shield", + "pond_item_silvers", + "pond_item_bottle_filled", + "pond_item_sword", + "pond_of_wishing_happiness", + "pond_of_wishing_choice", + "pond_of_wishing_bombs", + "pond_of_wishing_arrows", + "pond_of_wishing_full_upgrades", + "mountain_old_man_first", + "mountain_old_man_deadend", + "mountain_old_man_turn_right", + "mountain_old_man_lost_and_alone", + "mountain_old_man_drop_off", + "mountain_old_man_in_his_cave_pre_agahnim", + "mountain_old_man_in_his_cave", + "mountain_old_man_in_his_cave_post_agahnim", + "tavern_old_man_awake", + "tavern_old_man_unactivated_flute", + "tavern_old_man_know_tree_unactivated_flute", + "tavern_old_man_have_flute", + "chicken_hut_lady", + "running_man", + "game_race_sign", + "sign_bumper_cave", + "sign_catfish", + "sign_north_village_of_outcasts", + "sign_south_of_bumper_cave", + "sign_east_of_pyramid", + "sign_east_of_bomb_shop", + "sign_east_of_mire", + "sign_village_of_outcasts", + "sign_before_wishing_pond", + "sign_before_catfish_area", + "castle_wall_guard", + "gate_guard", + "telepathic_tile_eastern_palace", + "telepathic_tile_tower_of_hera_floor_4", + "hylian_text_1", + "mastersword_pedestal_translated", + "telepathic_tile_spectacle_rock", + "telepathic_tile_swamp_entrance", + "telepathic_tile_thieves_town_upstairs", + "telepathic_tile_misery_mire", + "hylian_text_2", + "desert_entry_translated", + "telepathic_tile_under_ganon", + "telepathic_tile_palace_of_darkness", + "telepathic_tile_desert_bonk_torch_room", + "telepathic_tile_castle_tower", + "telepathic_tile_ice_large_room", + "telepathic_tile_turtle_rock", + "telepathic_tile_ice_entrance", + "telepathic_tile_ice_stalfos_knights_room", + "telepathic_tile_tower_of_hera_entrance", + "houlihan_room", + "caught_a_bee", + "caught_a_fairy", + "no_empty_bottles", + "game_race_boy_time", + "game_race_girl", + "game_race_boy_success", + "game_race_boy_failure", + "game_race_boy_already_won", + "game_race_boy_sneaky", + "bottle_vendor_choice", + "bottle_vendor_get", + "bottle_vendor_no", + "bottle_vendor_already_collected", + "bottle_vendor_bee", + "bottle_vendor_fish", + "hobo_item_get_bottle", + "blacksmiths_what_you_want", + "blacksmiths_paywall", + "blacksmiths_extra_okay", + "blacksmiths_tempered_already", + "blacksmiths_temper_no", + "blacksmiths_bogart_sword", + "blacksmiths_get_sword", + "blacksmiths_shop_before_saving", + "blacksmiths_shop_saving", + "blacksmiths_collect_frog", + "blacksmiths_still_working", + "blacksmiths_saving_bows", + "blacksmiths_hammer_anvil", + "dark_flute_boy_storytime", + "dark_flute_boy_get_shovel", + "dark_flute_boy_no_get_shovel", + "dark_flute_boy_flute_not_found", + "dark_flute_boy_after_shovel_get", + "shop_fortune_teller_lw_hint_0", + "shop_fortune_teller_lw_hint_1", + "shop_fortune_teller_lw_hint_2", + "shop_fortune_teller_lw_hint_3", + "shop_fortune_teller_lw_hint_4", + "shop_fortune_teller_lw_hint_5", + "shop_fortune_teller_lw_hint_6", + "shop_fortune_teller_lw_hint_7", + "shop_fortune_teller_lw_no_rupees", + "shop_fortune_teller_lw", + "shop_fortune_teller_lw_post_hint", + "shop_fortune_teller_lw_no", + "shop_fortune_teller_lw_hint_8", + "shop_fortune_teller_lw_hint_9", + "shop_fortune_teller_lw_hint_10", + "shop_fortune_teller_lw_hint_11", + "shop_fortune_teller_lw_hint_12", + "shop_fortune_teller_lw_hint_13", + "shop_fortune_teller_lw_hint_14", + "shop_fortune_teller_lw_hint_15", + "dark_sanctuary", + "dark_sanctuary_hint_0", + "dark_sanctuary_no", + "dark_sanctuary_hint_1", + "dark_sanctuary_yes", + "dark_sanctuary_hint_2", + "sick_kid_no_bottle", + "sick_kid_trade", + "sick_kid_post_trade", + "desert_thief_sitting", + "desert_thief_following", + "desert_thief_question", + "desert_thief_question_yes", + "desert_thief_after_item_get", + "desert_thief_reassure", + "hylian_text_3", + "tablet_ether_book", + "tablet_bombos_book", + "magic_bat_wake", + "magic_bat_give_half_magic", + "intro_main", + "intro_throne_room", + "intro_zelda_cell", + "intro_agahnim", + "pickup_purple_chest", + "bomb_shop", + "bomb_shop_big_bomb", + "bomb_shop_big_bomb_buy", + "item_get_big_bomb", + "kiki_second_extortion", + "kiki_second_extortion_no", + "kiki_second_extortion_yes", + "kiki_first_extortion", + "kiki_first_extortion_yes", + "kiki_first_extortion_no", + "kiki_leaving_screen", + "blind_in_the_cell", + "blind_by_the_light", + "blind_not_that_way", + "aginah_l1sword_no_book", + "aginah_l1sword_with_pendants", + "aginah", + "aginah_need_better_sword", + "aginah_have_better_sword", + "catfish", + "catfish_after_item", + "lumberjack_right", + "lumberjack_left", + "lumberjack_left_post_agahnim", + "fighting_brothers_right", + "fighting_brothers_right_opened", + "fighting_brothers_left", + "maiden_crystal_1", + "maiden_crystal_2", + "maiden_crystal_3", + "maiden_crystal_4", + "maiden_crystal_5", + "maiden_crystal_6", + "maiden_crystal_7", + "maiden_ending", + "maiden_confirm_understood", + "barrier_breaking", + "maiden_crystal_7_again", + "agahnim_zelda_teleport", + "agahnim_magic_running_away", + "agahnim_hide_and_seek_found", + "agahnim_defeated", + "agahnim_final_meeting", + "zora_meeting", + "zora_tells_cost", + "zora_get_flippers", + "zora_no_cash", + "zora_no_buy_item", + "kakariko_saharalasa_grandson", + "kakariko_saharalasa_grandson_next", + "dark_palace_tree_dude", + "fairy_wishing_ponds", + "fairy_wishing_ponds_no", + "pond_of_wishing_no", + "pond_of_wishing_return_item", + "pond_of_wishing_throw", + "pond_pre_item_silvers", + "pond_of_wishing_great_luck", + "pond_of_wishing_good_luck", + "pond_of_wishing_meh_luck", + "pond_of_wishing_bad_luck", + "pond_of_wishing_fortune", + "item_get_14_heart", + "item_get_24_heart", + "item_get_34_heart", + "item_get_whole_heart", + "item_get_sanc_heart", + "fairy_fountain_refill", + "death_mountain_bullied_no_pearl", + "death_mountain_bullied_with_pearl", + "death_mountain_bully_no_pearl", + "death_mountain_bully_with_pearl", + "shop_darkworld_enter", + "game_chest_village_of_outcasts", + "game_chest_no_cash", + "game_chest_not_played", + "game_chest_played", + "game_chest_village_of_outcasts_play", + "shop_first_time", + "shop_already_have", + "shop_buy_shield", + "shop_buy_red_potion", + "shop_buy_arrows", + "shop_buy_bombs", + "shop_buy_bee", + "shop_buy_heart", + "shop_first_no_bottle_buy", + "shop_buy_no_space", + "ganon_fall_in", + "ganon_phase_3", + "lost_woods_thief", + "blinds_hut_dude", + "end_triforce", + "toppi_fallen", + "kakariko_tavern_fisherman", + "thief_money", + "thief_desert_rupee_cave", + "thief_ice_rupee_cave", + "telepathic_tile_south_east_darkworld_cave", + "cukeman", + "cukeman_2", + "potion_shop_no_cash", + "kakariko_powdered_chicken", + "game_chest_south_of_kakariko", + "game_chest_play_yes", + "game_chest_play_no", + "game_chest_lost_woods", + "kakariko_flophouse_man_no_flippers", + "kakariko_flophouse_man", + "menu_start_2", + "menu_start_3", + "menu_pause", + "game_digging_choice", + "game_digging_start", + "game_digging_no_cash", + "game_digging_end_time", + "game_digging_come_back_later", + "game_digging_no_follower", + "menu_start_4", + "ganon_fall_in_alt", + "ganon_phase_3_alt", + "sign_east_death_mountain_bridge", + "fish_money", + "sign_ganons_tower", + "sign_ganon", + "ganon_phase_3_no_bow", + "ganon_phase_3_no_silvers_alt", + "ganon_phase_3_no_silvers", + "ganon_phase_3_silvers", + "murahdahla", + ] + def __init__(self): self._text = OrderedDict() self.setDefaultText() diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 175fa0116583..af21a17335fe 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -101,7 +101,6 @@ class StartingItems(OptionList): class MCPlandoConnections(PlandoConnections): entrances = set(connection[0] for connection in region_info["default_connections"]) exits = set(connection[1] for connection in region_info["default_connections"]) - shared_connections = False @classmethod def can_connect(cls, entrance, exit): diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 86a1cbb07a65..6aa0884c421c 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -31,7 +31,6 @@ def from_any(cls, data: typing.Any) -> Range: class OoTPlandoConnections(PlandoConnections): - shared_connections = False entrances = set([connection[1][0] for connection in entrance_shuffle_table]) exits = set([connection[2][0] for connection in entrance_shuffle_table if len(connection) > 2]) From fea2a346b7d485359fc120ea0529c15512455816 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 5 Mar 2024 00:21:30 -0600 Subject: [PATCH 05/38] remove PlandoConnection stub, finish texts support the only world to use it is Tunic anyways --- Generate.py | 5 ++--- Options.py | 39 +++++++++++++++++++++++++++++++++----- worlds/alttp/Rom.py | 2 +- worlds/generic/__init__.py | 1 - worlds/tunic/er_scripts.py | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Generate.py b/Generate.py index 0ac3f29b9c03..a7dda3a34fc1 100644 --- a/Generate.py +++ b/Generate.py @@ -25,7 +25,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister -from worlds.generic import PlandoConnection def mystery_argparse(): @@ -463,12 +462,12 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "A Link to the Past": - roll_alttp_settings(ret, game_weights, plando_options) + roll_alttp_settings(ret, game_weights) return ret -def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): +def roll_alttp_settings(ret: argparse.Namespace, weights): ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite = get_choice_legacy('sprite', weights, "Link") if 'random_sprite_on_event' in weights: diff --git a/Options.py b/Options.py index c360b27fb360..bbc75f99795c 100644 --- a/Options.py +++ b/Options.py @@ -900,11 +900,18 @@ class ItemSet(OptionSet): convert_name_groups = True -class PlandoTexts(Option[typing.Dict[str, str]], VerifyKeys): +class PlandoText(typing.NamedTuple): + at: str + text: str + percentage: int = 100 + + +class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): default: typing.List = [] supports_weighting = False + display_name = "Plando Texts" - def __init__(self, value: typing.Dict[str, str]): + def __init__(self, value: typing.List[PlandoText]): self.value = deepcopy(value) super().__init__() @@ -917,15 +924,22 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P f"so text for {player_name} will be ignored.") @classmethod - def from_any(cls, data: typing.List[typing.Any]) -> Option[typing.Dict[str, str]]: - texts = {} + def from_any(cls, data: typing.List[typing.Any]) -> Option[typing.List[PlandoText]]: + texts = [] if type(data) == list: for text in data: if type(text) == dict: if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: - texts[at] = text.get("text", "") + texts.append(PlandoText( + at, + text.get("text"), + text.get("percentage", 100) + )) + elif type(text) == PlandoText: + if random.random() < float(text.percentage/100): + texts.append(text) else: raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}") cls.verify_keys([text.at for text in texts]) @@ -933,6 +947,18 @@ def from_any(cls, data: typing.List[typing.Any]) -> Option[typing.Dict[str, str] else: raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") + def get_option_name(self, value) -> str: + return str({text.at: text.text for text in value}) + + def __iter__(self): + yield from self.value + + def __getitem__(self, item): + return self.value.__getitem__(item) + + def __len__(self): + return self.value.__len__() + class ConnectionsMeta(AssembleOptions): def __new__(mcs, name, bases, attrs): @@ -1058,6 +1084,9 @@ def get_option_name(self, value): "=>", connection.exit) for connection in value]) + def __getitem__(self, item): + return self.value.__getitem__(item) + def __iter__(self): yield from self.value diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index ff4947bb0198..9c5db418d305 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2535,7 +2535,7 @@ def hint_text(dest, ped_hint=False): tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}" tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}" - for at, text in world.plando_texts[player].items(): + for at, text, _ in world.plando_texts[player]: if at not in tt: raise Exception(f"No text target \"{at}\" found.") diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index 00b1f6572415..bea2cede675e 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -5,7 +5,6 @@ from ..AutoWorld import World, WebWorld from NetUtils import SlotType -from Options import PlandoConnection class GenericWeb(WebWorld): advanced_settings = Tutorial('Advanced YAML Guide', diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index d2b854f5df0e..23fa24faa69c 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -4,7 +4,7 @@ from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_ur, \ dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur from .er_rules import set_er_region_rules -from worlds.generic import PlandoConnection +from Options import PlandoConnection if TYPE_CHECKING: from . import TunicWorld From 184129abbd3bd67e34c3cc3c9bfefc45219ae72a Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 5 Mar 2024 00:58:55 -0600 Subject: [PATCH 06/38] actually error check direction --- Options.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Options.py b/Options.py index bbc75f99795c..3a633a100f54 100644 --- a/Options.py +++ b/Options.py @@ -1026,6 +1026,11 @@ def validate_plando_connections(cls, connections): for connection in connections: entrance = connection.entrance exit = connection.exit + direction = connection.direction + if direction not in (PlandoConnections.Direction.Entrance, + PlandoConnections.Direction.Exit, + PlandoConnections.Direction.Both): + raise ValueError(f"Unknown direction: {direction}") if entrance in used_entrances: raise ValueError(f"Duplicate Entrance {entrance} not allowed.") if exit in used_exits: From 6fee5a2bafafda01541cb071e58085eeee415b5f Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:05:06 -0600 Subject: [PATCH 07/38] Update __init__.py --- worlds/generic/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/generic/__init__.py b/worlds/generic/__init__.py index bea2cede675e..b857147da767 100644 --- a/worlds/generic/__init__.py +++ b/worlds/generic/__init__.py @@ -6,6 +6,7 @@ from ..AutoWorld import World, WebWorld from NetUtils import SlotType + class GenericWeb(WebWorld): advanced_settings = Tutorial('Advanced YAML Guide', 'A guide to reading YAML files and editing them to fully customize your game.', From 5571fc54e254709dc92a59702d494d6bc94d200f Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:15:51 -0600 Subject: [PATCH 08/38] remove now unused imports --- Generate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Generate.py b/Generate.py index a7dda3a34fc1..9c55a2e8f6f7 100644 --- a/Generate.py +++ b/Generate.py @@ -21,9 +21,7 @@ from Main import main as ERmain from settings import get_settings from Utils import parse_yamls, version_tuple, __version__, tuplize_version -from worlds.alttp import Options as LttPOptions from worlds.alttp.EntranceRandomizer import parse_arguments -from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister From 51cda33f14d2098c39ff41aa6f76738d6b48e708 Mon Sep 17 00:00:00 2001 From: beauxq Date: Tue, 5 Mar 2024 07:45:37 -0800 Subject: [PATCH 09/38] fix some issues --- Options.py | 121 +++++++++++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/Options.py b/Options.py index 3a633a100f54..5bb785708fa0 100644 --- a/Options.py +++ b/Options.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from schema import And, Optional, Or, Schema +from typing_extensions import Self from Utils import get_fuzzy_results, is_iterable_of_str @@ -906,13 +907,18 @@ class PlandoText(typing.NamedTuple): percentage: int = 100 +PlandoTextsFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any +] + + class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys): - default: typing.List = [] + default = () supports_weighting = False display_name = "Plando Texts" - def __init__(self, value: typing.List[PlandoText]): - self.value = deepcopy(value) + def __init__(self, value: typing.Iterable[PlandoText]) -> None: + self.value = list(deepcopy(value)) super().__init__() def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: @@ -924,20 +930,20 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P f"so text for {player_name} will be ignored.") @classmethod - def from_any(cls, data: typing.List[typing.Any]) -> Option[typing.List[PlandoText]]: - texts = [] - if type(data) == list: + def from_any(cls, data: PlandoTextsFromAnyType) -> Self: + texts: typing.List[PlandoText] = [] + if isinstance(data, typing.Iterable): for text in data: - if type(text) == dict: + if isinstance(text, typing.Mapping): if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: texts.append(PlandoText( at, - text.get("text"), + text.get("text", ""), text.get("percentage", 100) )) - elif type(text) == PlandoText: + elif isinstance(text, PlandoText): if random.random() < float(text.percentage/100): texts.append(text) else: @@ -947,16 +953,17 @@ def from_any(cls, data: typing.List[typing.Any]) -> Option[typing.List[PlandoTex else: raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}") - def get_option_name(self, value) -> str: + @classmethod + def get_option_name(cls, value: typing.List[PlandoText]) -> str: return str({text.at: text.text for text in value}) - def __iter__(self): + def __iter__(self) -> typing.Iterator[PlandoText]: yield from self.value - def __getitem__(self, item): - return self.value.__getitem__(item) + def __getitem__(self, index: typing.SupportsIndex) -> PlandoText: + return self.value.__getitem__(index) - def __len__(self): + def __len__(self) -> int: return self.value.__len__() @@ -980,6 +987,11 @@ class PlandoConnection(typing.NamedTuple): percentage: int = 100 +PlandoConFromAnyType = typing.Union[ + typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any +] + + class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta): """Generic connections plando. Format is: - entrance: "Entrance Name" @@ -995,61 +1007,61 @@ class Direction: display_name = "Plando Connections" - default: typing.List[PlandoConnection] = [] + default = () supports_weighting = False entrances: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] exits: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] - def __init__(self, value: typing.List[PlandoConnection]): - self.value = deepcopy(value) + def __init__(self, value: typing.Iterable[PlandoConnection]): + self.value = list(deepcopy(value)) super(PlandoConnections, self).__init__() @classmethod - def validate_entrance_name(cls, entrance): + def validate_entrance_name(cls, entrance: str) -> bool: return entrance.lower() in cls.entrances @classmethod - def validate_exit_name(cls, exit): + def validate_exit_name(cls, exit: str) -> bool: return exit.lower() in cls.exits @classmethod - def can_connect(cls, entrance, exit): + def can_connect(cls, entrance: str, exit: str) -> bool: """Checks that a given entrance can connect to a given exit. By default, this will always return true unless overridden.""" return True @classmethod - def validate_plando_connections(cls, connections): - used_entrances = [] - used_exits = [] + def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None: + used_entrances: typing.List[str] = [] + used_exits: typing.List[str] = [] for connection in connections: - entrance = connection.entrance - exit = connection.exit - direction = connection.direction - if direction not in (PlandoConnections.Direction.Entrance, - PlandoConnections.Direction.Exit, - PlandoConnections.Direction.Both): - raise ValueError(f"Unknown direction: {direction}") - if entrance in used_entrances: - raise ValueError(f"Duplicate Entrance {entrance} not allowed.") - if exit in used_exits: - raise ValueError(f"Duplicate Exit {exit} not allowed.") - used_entrances.append(entrance) - used_exits.append(exit) - if not cls.validate_entrance_name(entrance): - raise ValueError(f"{entrance.title()} is not a valid entrance.") - if not cls.validate_exit_name(exit): - raise ValueError(f"{exit.title()} is not a valid exit.") - if not cls.can_connect(entrance, exit): - raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") + entrance = connection.entrance + exit = connection.exit + direction = connection.direction + if direction not in (PlandoConnections.Direction.Entrance, + PlandoConnections.Direction.Exit, + PlandoConnections.Direction.Both): + raise ValueError(f"Unknown direction: {direction}") + if entrance in used_entrances: + raise ValueError(f"Duplicate Entrance {entrance} not allowed.") + if exit in used_exits: + raise ValueError(f"Duplicate Exit {exit} not allowed.") + used_entrances.append(entrance) + used_exits.append(exit) + if not cls.validate_entrance_name(entrance): + raise ValueError(f"{entrance.title()} is not a valid entrance.") + if not cls.validate_exit_name(exit): + raise ValueError(f"{exit.title()} is not a valid exit.") + if not cls.can_connect(entrance, exit): + raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.") @classmethod - def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoConnection]]: - if type(data) == list: - value = [] + def from_any(cls, data: PlandoConFromAnyType) -> Self: + if isinstance(data, typing.Iterable): + value: typing.List[PlandoConnection] = [] for connection in data: - if type(connection) == dict: + if isinstance(connection, typing.Mapping): percentage = connection.get("percentage", 100) if random.random() < float(percentage / 100): entrance = connection.get("entrance", None) @@ -1064,7 +1076,7 @@ def from_any(cls, data: typing.Any) -> Option[typing.List[PlandoConnection]]: direction, percentage )) - elif type(connection) == PlandoConnection: + elif isinstance(connection, PlandoConnection): if random.random() < float(connection.percentage / 100): value.append(connection) else: @@ -1082,20 +1094,21 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P logging.warning(f"The plando connections module is turned off, " f"so connections for {player_name} will be ignored.") - def get_option_name(self, value): + @classmethod + def get_option_name(cls, value: typing.List[PlandoConnection]) -> str: return ", ".join(["%s %s %s" % (connection.entrance, - "<=>" if connection.direction == self.Direction.Both else - "<=" if connection.direction == self.Direction.Exit else + "<=>" if connection.direction == cls.Direction.Both else + "<=" if connection.direction == cls.Direction.Exit else "=>", connection.exit) for connection in value]) - def __getitem__(self, item): - return self.value.__getitem__(item) + def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection: + return self.value.__getitem__(index) - def __iter__(self): + def __iter__(self) -> typing.Iterator[PlandoConnection]: yield from self.value - def __len__(self): + def __len__(self) -> int: return len(self.value) From 1b88395dac38907ccb6144eb2841ea2335d8d9f8 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:05:03 -0600 Subject: [PATCH 10/38] support KDL3 --- worlds/kdl3/Options.py | 7 ++++++- worlds/kdl3/Regions.py | 4 ++-- worlds/kdl3/test/__init__.py | 3 +-- worlds/kdl3/test/test_locations.py | 6 ++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/worlds/kdl3/Options.py b/worlds/kdl3/Options.py index 336bd33bc583..e0a4f12f15dc 100644 --- a/worlds/kdl3/Options.py +++ b/worlds/kdl3/Options.py @@ -2,10 +2,14 @@ from dataclasses import dataclass from Options import DeathLink, Choice, Toggle, OptionDict, Range, PlandoBosses, DefaultOnToggle, \ - PerGameCommonOptions + PerGameCommonOptions, PlandoConnections from .Names import LocationName +class KDL3PlandoConnections(PlandoConnections): + entrances = exits = {f"{i} {j}" for i in LocationName.level_names for j in range(1, 7)} + + class Goal(Choice): """ Zero: collect the Heart Stars, and defeat Zero in the Hyper Zone. @@ -400,6 +404,7 @@ class Gifting(Toggle): @dataclass class KDL3Options(PerGameCommonOptions): + plando_connections: KDL3PlandoConnections death_link: DeathLink game_language: GameLanguage goal: Goal diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index ed0d86586615..1f566f3a4390 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -117,8 +117,8 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte } possible_stages = [default_levels[level][stage] for level in default_levels for stage in range(6)] - if world.multiworld.plando_connections[world.player]: - for connection in world.multiworld.plando_connections[world.player]: + if world.options.plando_connections: + for connection in world.options.plando_connections: try: entrance_world, entrance_stage = connection.entrance.rsplit(" ", 1) stage_world, stage_stage = connection.exit.rsplit(" ", 1) diff --git a/worlds/kdl3/test/__init__.py b/worlds/kdl3/test/__init__.py index 11a17e63b7fa..4d3f4d70faae 100644 --- a/worlds/kdl3/test/__init__.py +++ b/worlds/kdl3/test/__init__.py @@ -2,7 +2,7 @@ from argparse import Namespace from BaseClasses import MultiWorld, PlandoOptions, CollectionState -from test.TestBase import WorldTestBase +from test.bases import WorldTestBase from test.general import gen_steps from worlds import AutoWorld from worlds.AutoWorld import call_all @@ -32,6 +32,5 @@ def world_setup(self, seed: typing.Optional[int] = None) -> None: }) self.multiworld.set_options(args) self.multiworld.plando_options = PlandoOptions.connections - self.multiworld.plando_connections = self.options["plando_connections"] if "plando_connections" in self.options.keys() else [] for step in gen_steps: call_all(self.multiworld, step) diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index 543f0d83926d..dd247ae0771f 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -1,5 +1,5 @@ from . import KDL3TestBase -from worlds.generic import PlandoConnection +from Options import PlandoConnection from ..Names import LocationName import typing @@ -48,12 +48,10 @@ class TestShiro(KDL3TestBase): options = { "open_world": False, "plando_connections": [ - [], - [ PlandoConnection("Grass Land 1", "Iceberg 5", "both"), PlandoConnection("Grass Land 2", "Ripple Field 5", "both"), PlandoConnection("Grass Land 3", "Grass Land 1", "both") - ]], + ], "stage_shuffle": "shuffled", "plando_options": "connections" } From 65bc9bf1553302ab3080070ef551435d3a8e0e0b Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:15:05 -0600 Subject: [PATCH 11/38] Update Options.py Co-authored-by: Doug Hoskisson --- Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Options.py b/Options.py index 2a2301a0b690..7510d63ba776 100644 --- a/Options.py +++ b/Options.py @@ -1010,8 +1010,8 @@ class Direction: default = () supports_weighting = False - entrances: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] - exits: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]] + entrances: typing.ClassVar[typing.AbstractSet[str]] + exits: typing.ClassVar[typing.AbstractSet[str]] def __init__(self, value: typing.Iterable[PlandoConnection]): self.value = list(deepcopy(value)) From e0bf453390f03b8044237b28be47c5c53f5365f1 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:07:04 -0500 Subject: [PATCH 12/38] support tunic plando connections --- worlds/tunic/er_scripts.py | 2 +- worlds/tunic/options.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 7ca09caa11bd..fc18c7f0297a 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -221,7 +221,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: start_region = "Overworld" connected_regions.update(add_dependent_regions(start_region, logic_rules)) - plando_connections = world.multiworld.plando_connections[world.player] + plando_connections = world.options.plando_connections # universal tracker support stuff, don't need to care about region dependency if hasattr(world.multiworld, "re_gen_passthrough"): diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 779e632326db..ccec964a98f7 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -1,7 +1,8 @@ from dataclasses import dataclass -from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PerGameCommonOptions - +from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections, + PerGameCommonOptions) +from .er_data import portal_mapping class SwordProgression(DefaultOnToggle): """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new @@ -137,6 +138,10 @@ class LaurelsLocation(Choice): default = 0 +class TUNICPlandoConnections(PlandoConnections): + entrances = {portal.name for portal in portal_mapping} + exits = {portal.name for portal in portal_mapping} + @dataclass class TunicOptions(PerGameCommonOptions): sword_progression: SwordProgression From ed582ae69c8f49505cfe7a274557b8f1deafcbde Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:07:24 -0500 Subject: [PATCH 13/38] forgot the option --- worlds/tunic/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index ccec964a98f7..58b5c2acebea 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -159,3 +159,4 @@ class TunicOptions(PerGameCommonOptions): maskless: Maskless laurels_location: LaurelsLocation start_inventory_from_pool: StartInventoryPool + plando_connections: TUNICPlandoConnections From 4cf661630ffc08dce6b3dbd6cb0d2023f0c82311 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:25:05 -0500 Subject: [PATCH 14/38] missed cases --- worlds/tunic/er_scripts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index fc18c7f0297a..3863c58f9e0c 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -226,7 +226,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # universal tracker support stuff, don't need to care about region dependency if hasattr(world.multiworld, "re_gen_passthrough"): if "TUNIC" in world.multiworld.re_gen_passthrough: - plando_connections.clear() + plando_connections.value.clear() # universal tracker stuff, won't do anything in normal gen for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items(): portal_name1 = "" @@ -242,7 +242,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # shops have special handling if not portal_name2 and portal2 == "Shop, Previous Region_": portal_name2 = "Shop Portal" - plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both")) + plando_connections.value.append(PlandoConnection(portal_name1, portal_name2, "both")) non_dead_end_regions = set() for region_name, region_info in tunic_er_regions.items(): From c8a0e53ca7c0cb409c573aa11ffd28d8e7cb2461 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:02:04 -0500 Subject: [PATCH 15/38] scope Direction under PlandoConnection --- Options.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Options.py b/Options.py index 95b13a67942f..d010bf03459d 100644 --- a/Options.py +++ b/Options.py @@ -997,9 +997,14 @@ def __new__(mcs, name, bases, attrs): class PlandoConnection(typing.NamedTuple): + class Direction: + entrance = "entrance" + exit = "exit" + both = "both" + entrance: str exit: str - direction: str # entrance, exit or both + direction: Direction # entrance, exit or both percentage: int = 100 @@ -1016,10 +1021,6 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect percentage: 100 Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. Percentage is an integer from 1 to 100, and defaults to 100 when omitted.""" - class Direction: - Entrance = "entrance" - Exit = "exit" - Both = "both" display_name = "Plando Connections" @@ -1055,9 +1056,9 @@ def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnecti entrance = connection.entrance exit = connection.exit direction = connection.direction - if direction not in (PlandoConnections.Direction.Entrance, - PlandoConnections.Direction.Exit, - PlandoConnections.Direction.Both): + if direction not in (PlandoConnection.Direction.entrance, + PlandoConnection.Direction.exit, + PlandoConnection.Direction.both): raise ValueError(f"Unknown direction: {direction}") if entrance in used_entrances: raise ValueError(f"Duplicate Entrance {entrance} not allowed.") From a76b0088a0c8f2d07474de24e507046f5baebcb2 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:29:37 -0500 Subject: [PATCH 16/38] invert first if, fix missed Direction reference --- Options.py | 56 +++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Options.py b/Options.py index d010bf03459d..e567b65f4b83 100644 --- a/Options.py +++ b/Options.py @@ -1075,34 +1075,34 @@ def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnecti @classmethod def from_any(cls, data: PlandoConFromAnyType) -> Self: - if isinstance(data, typing.Iterable): - value: typing.List[PlandoConnection] = [] - for connection in data: - if isinstance(connection, typing.Mapping): - percentage = connection.get("percentage", 100) - if random.random() < float(percentage / 100): - entrance = connection.get("entrance", None) - exit = connection.get("exit", None) - direction = connection.get("direction", "both") - - if not entrance or not exit: - raise Exception("Plando connection must have an entrance and an exit.") - value.append(PlandoConnection( - entrance, - exit, - direction, - percentage - )) - elif isinstance(connection, PlandoConnection): - if random.random() < float(connection.percentage / 100): - value.append(connection) - else: - raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") - cls.validate_plando_connections(value) - return cls(value) - else: + if not isinstance(data, typing.Iterable): raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.") + value: typing.List[PlandoConnection] = [] + for connection in data: + if isinstance(connection, typing.Mapping): + percentage = connection.get("percentage", 100) + if random.random() < float(percentage / 100): + entrance = connection.get("entrance", None) + exit = connection.get("exit", None) + direction = connection.get("direction", "both") + + if not entrance or not exit: + raise Exception("Plando connection must have an entrance and an exit.") + value.append(PlandoConnection( + entrance, + exit, + direction, + percentage + )) + elif isinstance(connection, PlandoConnection): + if random.random() < float(connection.percentage / 100): + value.append(connection) + else: + raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.") + cls.validate_plando_connections(value) + return cls(value) + def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: from BaseClasses import PlandoOptions if not (PlandoOptions.connections & plando_options): @@ -1114,8 +1114,8 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P @classmethod def get_option_name(cls, value: typing.List[PlandoConnection]) -> str: return ", ".join(["%s %s %s" % (connection.entrance, - "<=>" if connection.direction == cls.Direction.Both else - "<=" if connection.direction == cls.Direction.Exit else + "<=>" if connection.direction == PlandoConnection.Direction.both else + "<=" if connection.direction == PlandoConnection.Direction.exit else "=>", connection.exit) for connection in value]) From 78591298b38abef98ee1d6b32d529417c4ac7a99 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 15 Mar 2024 14:47:08 -0500 Subject: [PATCH 17/38] swap to options API for plando connections --- worlds/messenger/options.py | 27 +++++++++++++++++++--- worlds/messenger/portals.py | 45 +++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index c56ee700438f..2e7d610184ab 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,10 +1,11 @@ from dataclasses import dataclass -from typing import Dict +from typing import Dict, Self from schema import And, Optional, Or, Schema -from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, Range, \ - StartInventoryPool, Toggle +from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ + PlandoConnections, Range, StartInventoryPool, Toggle +from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS class MessengerAccessibility(Accessibility): @@ -13,6 +14,25 @@ class MessengerAccessibility(Accessibility): __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") +class MessengerPlandoConnections(PlandoConnections): + """ + Plando connections to be used with portal shuffle. Direction is ignored. + List of valid connections can be found here: . + The entering Portal should *not* have "Portal" appended. + For the exits, those in PORTALS must have "Portal" added to the end, those in SHOP_POINTS should be " - Shop", and those in checkpoint should be " - Checkpoint". + Format is: + - entrance: Riviere Turquoise + exit: Howling Grotto - Wingsuit Shop + """ + portals = [f"{portal} Portal" for portal in PORTALS] + shop_points = [f"{area} - {spot} Shop" for area, spots in SHOP_POINTS.items() for spot in spots] + checkpoints = [f"{area} - {spot} Checkpoint" for area, spots in CHECKPOINTS.items() for spot in spots] + portal_entrances = PORTALS + portal_exits = portals + shop_points + checkpoints + entrances = portal_entrances + exits = portal_exits + + class Logic(Choice): """ The level of logic to use when determining what locations in your world are accessible. @@ -184,6 +204,7 @@ class PlannedShopPrices(OptionDict): @dataclass class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): accessibility: MessengerAccessibility + plando_connections: MessengerPlandoConnections start_inventory: StartInventoryPool logic_level: Logic shuffle_shards: MegaShards diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 64438b018400..a4a2a649a299 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -1,8 +1,7 @@ from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions -from .options import ShufflePortals -from ..generic import PlandoConnection +from Options import PlandoConnection if TYPE_CHECKING: from . import MessengerWorld @@ -18,24 +17,6 @@ ] -REGION_ORDER = [ - "Autumn Hills", - "Forlorn Temple", - "Catacombs", - "Bamboo Creek", - "Howling Grotto", - "Quillshroom Marsh", - "Searing Crags", - "Glacial Peak", - "Tower of Time", - "Cloud Ruins", - "Underworld", - "Riviere Turquoise", - "Elemental Skylands", - "Sunken Shrine", -] - - SHOP_POINTS = { "Autumn Hills": [ "Climbing Claws", @@ -204,7 +185,27 @@ } +REGION_ORDER = [ + "Autumn Hills", + "Forlorn Temple", + "Catacombs", + "Bamboo Creek", + "Howling Grotto", + "Quillshroom Marsh", + "Searing Crags", + "Glacial Peak", + "Tower of Time", + "Cloud Ruins", + "Underworld", + "Riviere Turquoise", + "Elemental Skylands", + "Sunken Shrine", +] + + def shuffle_portals(world: "MessengerWorld") -> None: + from .options import ShufflePortals, MessengerPlandoConnections + def create_mapping(in_portal: str, warp: str) -> None: nonlocal available_portals parent = out_to_parent[warp] @@ -229,7 +230,7 @@ def create_mapping(in_portal: str, warp: str) -> None: def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: for connection in plando_connections: - if connection.entrance not in PORTALS: + if connection.entrance not in MessengerPlandoConnections.portals: continue # let it crash here if input is invalid create_mapping(connection.entrance, connection.exit) @@ -244,7 +245,7 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: out_to_parent = {checkpoint: parent for parent, checkpoints in shop_points.items() for checkpoint in checkpoints} available_portals = [val for zone in shop_points.values() for val in zone] - plando = world.multiworld.plando_connections[world.player] + plando = world.options.plando_connections if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) world.multiworld.plando_connections[world.player] = [connection for connection in plando From 194de1b4249aefd9324a99091747084eda214b42 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 15 Mar 2024 17:14:20 -0500 Subject: [PATCH 18/38] fix a bunch of bugs --- worlds/messenger/options.py | 8 ++++---- worlds/messenger/portals.py | 32 +++++++++++++++++++------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 2e7d610184ab..653377e370fa 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -19,14 +19,14 @@ class MessengerPlandoConnections(PlandoConnections): Plando connections to be used with portal shuffle. Direction is ignored. List of valid connections can be found here: . The entering Portal should *not* have "Portal" appended. - For the exits, those in PORTALS must have "Portal" added to the end, those in SHOP_POINTS should be " - Shop", and those in checkpoint should be " - Checkpoint". + For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end. Format is: - entrance: Riviere Turquoise - exit: Howling Grotto - Wingsuit Shop + exit: Wingsuit """ portals = [f"{portal} Portal" for portal in PORTALS] - shop_points = [f"{area} - {spot} Shop" for area, spots in SHOP_POINTS.items() for spot in spots] - checkpoints = [f"{area} - {spot} Checkpoint" for area, spots in CHECKPOINTS.items() for spot in spots] + shop_points = [point for points in SHOP_POINTS.values() for point in points] + checkpoints = [point for points in CHECKPOINTS.values() for point in points] portal_entrances = PORTALS portal_exits = portals + shop_points + checkpoints entrances = portal_entrances diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index a4a2a649a299..3e735be2b05d 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions @@ -204,7 +205,7 @@ def shuffle_portals(world: "MessengerWorld") -> None: - from .options import ShufflePortals, MessengerPlandoConnections + from .options import ShufflePortals def create_mapping(in_portal: str, warp: str) -> None: nonlocal available_portals @@ -214,12 +215,12 @@ def create_mapping(in_portal: str, warp: str) -> None: if "Portal" in warp: exit_string += "Portal" world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00")) - elif warp_point in SHOP_POINTS[parent]: - exit_string += f"{warp_point} Shop" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp_point)}")) + elif warp in SHOP_POINTS[parent]: + exit_string += f"{warp} Shop" + world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) else: - exit_string += f"{warp_point} Checkpoint" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp_point)}")) + exit_string += f"{warp} Checkpoint" + world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) world.spoiler_portal_mapping[in_portal] = exit_string connect_portal(world, in_portal, exit_string) @@ -229,28 +230,33 @@ def create_mapping(in_portal: str, warp: str) -> None: available_portals = [port for port in available_portals if port not in shop_points[parent]] def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: + nonlocal plandoed_portals + for connection in plando_connections: - if connection.entrance not in MessengerPlandoConnections.portals: + if connection.entrance not in PORTALS: continue # let it crash here if input is invalid create_mapping(connection.entrance, connection.exit) world.plando_portals.append(connection.entrance) + plandoed_portals.append(connection.entrance) shuffle_type = world.options.shuffle_portals - shop_points = SHOP_POINTS.copy() + shop_points = deepcopy(SHOP_POINTS) for portal in PORTALS: shop_points[portal].append(f"{portal} Portal") if shuffle_type > ShufflePortals.option_shops: - shop_points.update(CHECKPOINTS) + for area, points in shop_points.items(): + points += CHECKPOINTS[area] out_to_parent = {checkpoint: parent for parent, checkpoints in shop_points.items() for checkpoint in checkpoints} available_portals = [val for zone in shop_points.values() for val in zone] plando = world.options.plando_connections - if plando and world.multiworld.plando_options & PlandoOptions.connections: + plandoed_portals = [] + if plando: handle_planned_portals(plando) - world.multiworld.plando_connections[world.player] = [connection for connection in plando - if connection.entrance not in PORTALS] - for portal in PORTALS: + world.options.plando_connections.value = [connection for connection in plando + if connection.entrance not in PORTALS] + for portal in [port for port in PORTALS if port not in plandoed_portals]: warp_point = world.random.choice(available_portals) create_mapping(portal, warp_point) From 8d3366fbd3fa6490de89f3d9f1888201a72c92f0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 15 Mar 2024 17:14:58 -0500 Subject: [PATCH 19/38] only do plando when setting is enabled --- worlds/messenger/portals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 3e735be2b05d..63801548e62b 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -252,7 +252,7 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: plando = world.options.plando_connections plandoed_portals = [] - if plando: + if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) world.options.plando_connections.value = [connection for connection in plando if connection.entrance not in PORTALS] From 3fb3c3e22c46ba966981c2abe48483950e45a827 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 16 Mar 2024 01:32:57 -0500 Subject: [PATCH 20/38] fix bad unused import --- worlds/messenger/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 653377e370fa..6d137fa344bd 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Dict, Self +from typing import Dict from schema import And, Optional, Or, Schema From a0d300210817b162274da93e713374f1c89bc834 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 23 Mar 2024 23:23:23 -0500 Subject: [PATCH 21/38] Apply suggestions from code review Co-authored-by: Scipio Wright --- worlds/tunic/er_scripts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index c420635736c8..bf9fd4ba6499 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -159,12 +159,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: start_region = "Overworld" connected_regions.update(add_dependent_regions(start_region, logic_rules)) - plando_connections = world.options.plando_connections + plando_connections = world.options.plando_connections.value # universal tracker support stuff, don't need to care about region dependency if hasattr(world.multiworld, "re_gen_passthrough"): if "TUNIC" in world.multiworld.re_gen_passthrough: - plando_connections.value.clear() + plando_connections.clear() # universal tracker stuff, won't do anything in normal gen for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items(): portal_name1 = "" @@ -180,7 +180,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # shops have special handling if not portal_name2 and portal2 == "Shop, Previous Region_": portal_name2 = "Shop Portal" - plando_connections.value.append(PlandoConnection(portal_name1, portal_name2, "both")) + plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both")) non_dead_end_regions = set() for region_name, region_info in tunic_er_regions.items(): From 97377157ded679e129bf766183dca076c039b299 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 3 Apr 2024 19:38:42 -0500 Subject: [PATCH 22/38] fix LttP circular imports --- worlds/alttp/Shops.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index dbe8cc1f9dfa..db2b5b680c1d 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -9,9 +9,9 @@ from BaseClasses import CollectionState from .SubClasses import ALttPLocation -from .EntranceShuffle import door_addresses + from .Items import item_name_groups -from .Options import small_key_shuffle, RandomizeShopInventories + from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows logger = logging.getLogger("Shops") @@ -66,6 +66,7 @@ def item_count(self) -> int: return 0 def get_bytes(self) -> List[int]: + from .EntranceShuffle import door_addresses # [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index] entrances = self.region.entrances config = self.item_count @@ -181,7 +182,7 @@ def push_shop_inventories(multiworld): def create_shops(multiworld, player: int): - + from .Options import RandomizeShopInventories player_shop_table = shop_table.copy() if multiworld.include_witch_hut[player]: player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False) @@ -304,6 +305,7 @@ class ShopData(NamedTuple): def set_up_shops(multiworld, player: int): + from .Options import small_key_shuffle # TODO: move hard+ mode changes for shields here, utilizing the new shops if multiworld.retro_bow[player]: @@ -426,7 +428,7 @@ def get_price_modifier(item): def get_price(multiworld, item, player: int, price_type=None): """Converts a raw Rupee price into a special price type""" - + from .Options import small_key_shuffle if price_type: price_types = [price_type] else: From aa894a0cfc4402019b8d71d9f0befab1753861ca Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 4 Apr 2024 00:23:01 -0500 Subject: [PATCH 23/38] allow passing lists into entrance and exit --- Options.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Options.py b/Options.py index e567b65f4b83..a6f19b6249d7 100644 --- a/Options.py +++ b/Options.py @@ -1084,7 +1084,11 @@ def from_any(cls, data: PlandoConFromAnyType) -> Self: percentage = connection.get("percentage", 100) if random.random() < float(percentage / 100): entrance = connection.get("entrance", None) + if is_iterable_except_str(entrance): + entrance = random.choice(entrance) exit = connection.get("exit", None) + if is_iterable_except_str(exit): + exit = random.choice(exit) direction = connection.get("direction", "both") if not entrance or not exit: From 4c5f40972b92172375895719d9e4a05f386c404a Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 4 Apr 2024 00:25:26 -0500 Subject: [PATCH 24/38] fix log warnings when option is empty but extant --- Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Options.py b/Options.py index a6f19b6249d7..5381368d6a7c 100644 --- a/Options.py +++ b/Options.py @@ -939,7 +939,7 @@ def __init__(self, value: typing.Iterable[PlandoText]) -> None: def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: from BaseClasses import PlandoOptions - if not (PlandoOptions.texts & plando_options): + if self.value and not (PlandoOptions.texts & plando_options): # plando is disabled but plando options were given so overwrite the options self.value = [] logging.warning(f"The plando texts module is turned off, " @@ -1109,7 +1109,7 @@ def from_any(cls, data: PlandoConFromAnyType) -> Self: def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: from BaseClasses import PlandoOptions - if not (PlandoOptions.connections & plando_options): + if self.value and not (PlandoOptions.connections & plando_options): # plando is disabled but plando options were given so overwrite the options self.value = [] logging.warning(f"The plando connections module is turned off, " From 1e4a2f87c685a065784aaea5e478d0a3072bc327 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:08:55 -0500 Subject: [PATCH 25/38] change plando text to use list of str for text --- Options.py | 9 ++++++--- worlds/alttp/Rom.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index f17793230c75..a91079f76c78 100644 --- a/Options.py +++ b/Options.py @@ -930,7 +930,7 @@ class ItemSet(OptionSet): class PlandoText(typing.NamedTuple): at: str - text: str + text: typing.List[str] percentage: int = 100 @@ -965,9 +965,12 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: if random.random() < float(text.get("percentage", 100)/100): at = text.get("at", None) if at is not None: + given_text = text.get("text", []) + if isinstance(given_text, str): + given_text = [given_text] texts.append(PlandoText( at, - text.get("text", ""), + given_text, text.get("percentage", 100) )) elif isinstance(text, PlandoText): @@ -982,7 +985,7 @@ def from_any(cls, data: PlandoTextsFromAnyType) -> Self: @classmethod def get_option_name(cls, value: typing.List[PlandoText]) -> str: - return str({text.at: text.text for text in value}) + return str({text.at: " ".join(text.text) for text in value}) def __iter__(self) -> typing.Iterator[PlandoText]: yield from self.value diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index ebfdd41d7374..16a15bd9f56c 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2543,7 +2543,7 @@ def hint_text(dest, ped_hint=False): if at not in tt: raise Exception(f"No text target \"{at}\" found.") else: - tt[at] = text + tt[at] = "\n".join(text) rom.write_bytes(0xE0000, tt.getBytes()) From 0591b2c8010ed8e2ae6b2c64a128ceef8d9e5f08 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:17:14 -0500 Subject: [PATCH 26/38] Update worlds/tunic/options.py Co-authored-by: Scipio Wright --- worlds/tunic/options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 6c60a9fec9e2..155f8e899df7 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -148,6 +148,8 @@ class ShuffleLadders(Toggle): class TUNICPlandoConnections(PlandoConnections): entrances = {portal.name for portal in portal_mapping} exits = {portal.name for portal in portal_mapping} + entrance.add("Shop Portal") + exits.add("Shop Portal") @dataclass From 9ae1db0563bb8d626fbe66f4635a456145c88ab0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:21:16 -0500 Subject: [PATCH 27/38] Update worlds/tunic/options.py Co-authored-by: Scipio Wright --- worlds/tunic/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 155f8e899df7..26117be7d4f6 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -148,7 +148,7 @@ class ShuffleLadders(Toggle): class TUNICPlandoConnections(PlandoConnections): entrances = {portal.name for portal in portal_mapping} exits = {portal.name for portal in portal_mapping} - entrance.add("Shop Portal") + entrances.add("Shop Portal") exits.add("Shop Portal") From a96c57fed3ec13f476fc3d5cd46b7212f6678bf4 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 21 Apr 2024 15:54:58 -0500 Subject: [PATCH 28/38] wrap in sorted --- Options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Options.py b/Options.py index a91079f76c78..de16c6690abb 100644 --- a/Options.py +++ b/Options.py @@ -1099,10 +1099,10 @@ def from_any(cls, data: PlandoConFromAnyType) -> Self: if random.random() < float(percentage / 100): entrance = connection.get("entrance", None) if is_iterable_except_str(entrance): - entrance = random.choice(entrance) + entrance = random.choice(sorted(entrance)) exit = connection.get("exit", None) if is_iterable_except_str(exit): - exit = random.choice(exit) + exit = random.choice(sorted(exit)) direction = connection.get("direction", "both") if not entrance or not exit: From 3a1bcff7232850cedf2c87bf95305d094c0ee4d5 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 3 May 2024 08:08:24 -0500 Subject: [PATCH 29/38] The Messenger: fix portal plando --- worlds/messenger/portals.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 97c46b1acebd..86b30fef7d45 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -5,7 +5,6 @@ from Options import PlandoConnection from .options import ShufflePortals - if TYPE_CHECKING: from . import MessengerWorld @@ -207,12 +206,9 @@ def shuffle_portals(world: "MessengerWorld") -> None: - from .options import ShufflePortals - """shuffles the output of the portals from the main hub""" def create_mapping(in_portal: str, warp: str) -> str: """assigns the chosen output to the input""" - nonlocal available_portals parent = out_to_parent[warp] exit_string = f"{parent.strip(' ')} - " @@ -232,8 +228,6 @@ def create_mapping(in_portal: str, warp: str) -> str: return parent def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: - nonlocal plandoed_portals - """checks the provided plando connections for portals and connects them""" for connection in plando_connections: if connection.entrance not in PORTALS: @@ -241,7 +235,6 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: # let it crash here if input is invalid create_mapping(connection.entrance, connection.exit) world.plando_portals.append(connection.entrance) - plandoed_portals.append(connection.entrance) shuffle_type = world.options.shuffle_portals shop_points = deepcopy(SHOP_POINTS) @@ -254,12 +247,9 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: available_portals = [val for zone in shop_points.values() for val in zone] world.random.shuffle(available_portals) - plando = world.options.plando_connections - plandoed_portals = [] + plando = world.options.plando_connections.value if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) - world.options.plando_connections.value = [connection for connection in plando - if connection.entrance not in PORTALS] for portal in PORTALS: if portal in world.plando_portals: From 2ad1270f2324fd38d292a328a7f06bb18e5c5cfc Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 3 May 2024 08:12:38 -0500 Subject: [PATCH 30/38] The Messenger: rename portal plando so that transition plando can be a separate option rather than trying to jank combine them --- worlds/messenger/options.py | 20 +++++++++++++++----- worlds/messenger/portals.py | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index aaf66c436996..401a3ecee1d2 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -4,7 +4,7 @@ from schema import And, Optional, Or, Schema from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ - PlandoConnections, Range, StartInventoryPool, Toggle + PlandoConnections, Range, StartInventoryPool, Toggle, Visibility from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS @@ -14,15 +14,19 @@ class MessengerAccessibility(Accessibility): __doc__ = Accessibility.__doc__.replace(f"default {Accessibility.default}", f"default {default}") -class MessengerPlandoConnections(PlandoConnections): +class PortalPlando(PlandoConnections): """ Plando connections to be used with portal shuffle. Direction is ignored. - List of valid connections can be found here: . + List of valid connections can be found here: https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/messenger/portals.py#L12. The entering Portal should *not* have "Portal" appended. For the exits, those in checkpoints and shops should just be the name of the spot, while portals should have " Portal" at the end. - Format is: + Example: - entrance: Riviere Turquoise exit: Wingsuit + - entrance: Sunken Shrine + exit: Sunny Day + - entrance: Searing Crags + exit: Glacial Peak Portal """ portals = [f"{portal} Portal" for portal in PORTALS] shop_points = [point for points in SHOP_POINTS.values() for point in points] @@ -33,6 +37,11 @@ class MessengerPlandoConnections(PlandoConnections): exits = portal_exits +# for back compatibility. To later be replaced with transition plando +class HiddenPortalPlando(PortalPlando): + visibility = Visibility.none + + class Logic(Choice): """ The level of logic to use when determining what locations in your world are accessible. @@ -209,7 +218,6 @@ class PlannedShopPrices(OptionDict): @dataclass class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): accessibility: MessengerAccessibility - plando_connections: MessengerPlandoConnections start_inventory: StartInventoryPool logic_level: Logic shuffle_shards: MegaShards @@ -226,3 +234,5 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): traps: Traps shop_price: ShopPrices shop_price_plan: PlannedShopPrices + portal_plando: PortalPlando + plando_connections: HiddenPortalPlando diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 86b30fef7d45..c4e28671f4f8 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -247,7 +247,9 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: available_portals = [val for zone in shop_points.values() for val in zone] world.random.shuffle(available_portals) - plando = world.options.plando_connections.value + plando = world.options.portal_plando.value + if not plando: + plando = world.options.plando_connections.value if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) From ddc8b00e31e760e8a78bc517b30a47f7fd2727f5 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 3 May 2024 09:44:23 -0500 Subject: [PATCH 31/38] fix circular import --- worlds/messenger/portals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index c4e28671f4f8..1da210cb23ff 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -3,7 +3,6 @@ from BaseClasses import CollectionState, PlandoOptions from Options import PlandoConnection -from .options import ShufflePortals if TYPE_CHECKING: from . import MessengerWorld @@ -207,6 +206,8 @@ def shuffle_portals(world: "MessengerWorld") -> None: """shuffles the output of the portals from the main hub""" + from .options import ShufflePortals + def create_mapping(in_portal: str, warp: str) -> str: """assigns the chosen output to the input""" parent = out_to_parent[warp] From f5c2dc464e5eb3f1e952dc27b19972c931c6b8f3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 3 May 2024 11:23:38 -0500 Subject: [PATCH 32/38] won't let me inherit entrances and exits :( --- worlds/messenger/options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 401a3ecee1d2..73adf4ebdf0a 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -40,6 +40,8 @@ class PortalPlando(PlandoConnections): # for back compatibility. To later be replaced with transition plando class HiddenPortalPlando(PortalPlando): visibility = Visibility.none + entrances = PortalPlando.entrances + exits = PortalPlando.exits class Logic(Choice): From 9a2593a2f49f066d1d3df78f21358dd5c96797a3 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 5 May 2024 02:44:10 -0500 Subject: [PATCH 33/38] Update __init__.py --- worlds/tunic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 20fbd82df2c3..ece5c8fd78f1 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -10,7 +10,7 @@ from .er_data import portal_mapping from .options import TunicOptions, EntranceRando from worlds.AutoWorld import WebWorld, World -from worlds.generic import PlandoConnection +from Options import PlandoConnection from decimal import Decimal, ROUND_HALF_UP From e6eac0c62132e16b8ced6090d98402f489e4398d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 10 May 2024 21:29:17 -0500 Subject: [PATCH 34/38] allow duplicate exits, apply to TUNIC --- Options.py | 5 ++++- worlds/tunic/options.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index febf330088d3..387284e35a09 100644 --- a/Options.py +++ b/Options.py @@ -1049,6 +1049,9 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect entrances: typing.ClassVar[typing.AbstractSet[str]] exits: typing.ClassVar[typing.AbstractSet[str]] + duplicate_exits: bool = False + """Whether or not exits should be allowed to be duplicate.""" + def __init__(self, value: typing.Iterable[PlandoConnection]): self.value = list(deepcopy(value)) super(PlandoConnections, self).__init__() @@ -1081,7 +1084,7 @@ def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnecti raise ValueError(f"Unknown direction: {direction}") if entrance in used_entrances: raise ValueError(f"Duplicate Entrance {entrance} not allowed.") - if exit in used_exits: + if not cls.duplicate_exits and exit in used_exits: raise ValueError(f"Duplicate Exit {exit} not allowed.") used_entrances.append(entrance) used_exits.append(exit) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 1db3a206feca..b16550adface 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -4,6 +4,7 @@ PerGameCommonOptions) from .er_data import portal_mapping + class SwordProgression(DefaultOnToggle): """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.""" internal_name = "sword_progression" @@ -138,7 +139,6 @@ class LaurelsLocation(Choice): default = 0 - class ShuffleLadders(Toggle): """Turns several ladders in the game into items that must be found before they can be climbed on. Adds more layers of progression to the game by blocking access to many areas early on. @@ -153,7 +153,9 @@ class TUNICPlandoConnections(PlandoConnections): entrances.add("Shop Portal") exits.add("Shop Portal") - + duplicate_exits = True + + @dataclass class TunicOptions(PerGameCommonOptions): sword_progression: SwordProgression From 3d2bde4cdad4b39902c5657b93c85fbba49f9dcb Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 13 May 2024 12:40:39 -0500 Subject: [PATCH 35/38] Update Options.py Co-authored-by: Doug Hoskisson --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index 387284e35a09..c0ffcafb5276 100644 --- a/Options.py +++ b/Options.py @@ -1003,7 +1003,7 @@ def __len__(self) -> int: class ConnectionsMeta(AssembleOptions): - def __new__(mcs, name, bases, attrs): + def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]): if name != "PlandoConnections": assert "entrances" in attrs, f"Please define valid entrances for {name}" attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"])) From 3b1dbd9805312bf252443f69d278c0da7e71ffc1 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 13 May 2024 21:12:41 -0500 Subject: [PATCH 36/38] update typing on direction with todo --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index c0ffcafb5276..fcfcce085e43 100644 --- a/Options.py +++ b/Options.py @@ -1023,7 +1023,7 @@ class Direction: entrance: str exit: str - direction: Direction # entrance, exit or both + direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped percentage: int = 100 From d3dfb96a49d17c0c61805dc981d58f061ef2efa0 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 15 May 2024 23:04:48 -0500 Subject: [PATCH 37/38] fix incorrect can_connect --- worlds/minecraft/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index af21a17335fe..9407097b4638 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -104,7 +104,7 @@ class MCPlandoConnections(PlandoConnections): @classmethod def can_connect(cls, entrance, exit): - if exit in region_info["illegal_connections"] and exit in region_info["illegal_connections"][exit]: + if exit in region_info["illegal_connections"] and entrance in region_info["illegal_connections"][exit]: return False return True From 2991ea2e3ec5f1a8711ea35c362071f112319e89 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 31 May 2024 13:58:17 -0500 Subject: [PATCH 38/38] adjust tunic shop handling --- worlds/tunic/__init__.py | 12 ++++++------ worlds/tunic/options.py | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 91457af60e7a..9ef5800955aa 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -70,17 +70,17 @@ class TunicWorld(World): seed_groups: Dict[str, SeedGroup] = {} def generate_early(self) -> None: - if self.multiworld.plando_connections[self.player]: - for index, cxn in enumerate(self.multiworld.plando_connections[self.player]): + if self.options.plando_connections: + for index, cxn in enumerate(self.options.plando_connections): # making shops second to simplify other things later if cxn.entrance.startswith("Shop"): replacement = PlandoConnection(cxn.exit, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) elif cxn.exit.startswith("Shop"): replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both") - self.multiworld.plando_connections[self.player].remove(cxn) - self.multiworld.plando_connections[self.player].insert(index, replacement) + self.options.plando_connections.value.remove(cxn) + self.options.plando_connections.value.insert(index, replacement) # Universal tracker stuff, shouldn't do anything in standard gen if hasattr(self.multiworld, "re_gen_passthrough"): diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 9e39c8b13dcb..b3b6b3b96fb0 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -174,10 +174,8 @@ class ShuffleLadders(Toggle): class TUNICPlandoConnections(PlandoConnections): - entrances = {portal.name for portal in portal_mapping} - exits = {portal.name for portal in portal_mapping} - entrances.add("Shop Portal") - exits.add("Shop Portal") + entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} + exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"} duplicate_exits = True