Skip to content

Commit 45b515b

Browse files
SilvrisbeauxqScipioWrightalwaysintreble
authored andcommitted
Core: move PlandoConnections and PlandoTexts to the options system (ArchipelagoMW#2904)
Co-authored-by: Doug Hoskisson <[email protected]> Co-authored-by: Scipio Wright <[email protected]> Co-authored-by: beauxq <[email protected]> Co-authored-by: alwaysintreble <[email protected]>
1 parent 61c6e52 commit 45b515b

19 files changed

+767
-71
lines changed

Generate.py

+2-27
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
from settings import get_settings
2424
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
2525
from worlds.alttp.EntranceRandomizer import parse_arguments
26-
from worlds.alttp.Text import TextTable
2726
from worlds.AutoWorld import AutoWorldRegister
28-
from worlds.generic import PlandoConnection
2927
from worlds import failed_world_loads
3028

3129

@@ -506,35 +504,12 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
506504
if PlandoOptions.items in plando_options:
507505
ret.plando_items = game_weights.get("plando_items", [])
508506
if ret.game == "A Link to the Past":
509-
roll_alttp_settings(ret, game_weights, plando_options)
510-
if PlandoOptions.connections in plando_options:
511-
ret.plando_connections = []
512-
options = game_weights.get("plando_connections", [])
513-
for placement in options:
514-
if roll_percentage(get_choice("percentage", placement, 100)):
515-
ret.plando_connections.append(PlandoConnection(
516-
get_choice("entrance", placement),
517-
get_choice("exit", placement),
518-
get_choice("direction", placement, "both")
519-
))
507+
roll_alttp_settings(ret, game_weights)
520508

521509
return ret
522510

523511

524-
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
525-
526-
ret.plando_texts = {}
527-
if PlandoOptions.texts in plando_options:
528-
tt = TextTable()
529-
tt.removeUnwantedText()
530-
options = weights.get("plando_texts", [])
531-
for placement in options:
532-
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
533-
at = str(get_choice_legacy("at", placement))
534-
if at not in tt:
535-
raise Exception(f"No text target \"{at}\" found.")
536-
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
537-
512+
def roll_alttp_settings(ret: argparse.Namespace, weights):
538513
ret.sprite_pool = weights.get('sprite_pool', [])
539514
ret.sprite = get_choice_legacy('sprite', weights, "Link")
540515
if 'random_sprite_on_event' in weights:

Options.py

+225-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from dataclasses import dataclass
1313

1414
from schema import And, Optional, Or, Schema
15+
from typing_extensions import Self
1516

1617
from Utils import get_fuzzy_results, is_iterable_except_str
1718

@@ -896,6 +897,228 @@ class ItemSet(OptionSet):
896897
convert_name_groups = True
897898

898899

900+
class PlandoText(typing.NamedTuple):
901+
at: str
902+
text: typing.List[str]
903+
percentage: int = 100
904+
905+
906+
PlandoTextsFromAnyType = typing.Union[
907+
typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoText, typing.Any]], typing.Any
908+
]
909+
910+
911+
class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
912+
default = ()
913+
supports_weighting = False
914+
display_name = "Plando Texts"
915+
916+
def __init__(self, value: typing.Iterable[PlandoText]) -> None:
917+
self.value = list(deepcopy(value))
918+
super().__init__()
919+
920+
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
921+
from BaseClasses import PlandoOptions
922+
if self.value and not (PlandoOptions.texts & plando_options):
923+
# plando is disabled but plando options were given so overwrite the options
924+
self.value = []
925+
logging.warning(f"The plando texts module is turned off, "
926+
f"so text for {player_name} will be ignored.")
927+
928+
@classmethod
929+
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
930+
texts: typing.List[PlandoText] = []
931+
if isinstance(data, typing.Iterable):
932+
for text in data:
933+
if isinstance(text, typing.Mapping):
934+
if random.random() < float(text.get("percentage", 100)/100):
935+
at = text.get("at", None)
936+
if at is not None:
937+
given_text = text.get("text", [])
938+
if isinstance(given_text, str):
939+
given_text = [given_text]
940+
texts.append(PlandoText(
941+
at,
942+
given_text,
943+
text.get("percentage", 100)
944+
))
945+
elif isinstance(text, PlandoText):
946+
if random.random() < float(text.percentage/100):
947+
texts.append(text)
948+
else:
949+
raise Exception(f"Cannot create plando text from non-dictionary type, got {type(text)}")
950+
cls.verify_keys([text.at for text in texts])
951+
return cls(texts)
952+
else:
953+
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")
954+
955+
@classmethod
956+
def get_option_name(cls, value: typing.List[PlandoText]) -> str:
957+
return str({text.at: " ".join(text.text) for text in value})
958+
959+
def __iter__(self) -> typing.Iterator[PlandoText]:
960+
yield from self.value
961+
962+
def __getitem__(self, index: typing.SupportsIndex) -> PlandoText:
963+
return self.value.__getitem__(index)
964+
965+
def __len__(self) -> int:
966+
return self.value.__len__()
967+
968+
969+
class ConnectionsMeta(AssembleOptions):
970+
def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict[str, typing.Any]):
971+
if name != "PlandoConnections":
972+
assert "entrances" in attrs, f"Please define valid entrances for {name}"
973+
attrs["entrances"] = frozenset((connection.lower() for connection in attrs["entrances"]))
974+
assert "exits" in attrs, f"Please define valid exits for {name}"
975+
attrs["exits"] = frozenset((connection.lower() for connection in attrs["exits"]))
976+
if "__doc__" not in attrs:
977+
attrs["__doc__"] = PlandoConnections.__doc__
978+
cls = super().__new__(mcs, name, bases, attrs)
979+
return cls
980+
981+
982+
class PlandoConnection(typing.NamedTuple):
983+
class Direction:
984+
entrance = "entrance"
985+
exit = "exit"
986+
both = "both"
987+
988+
entrance: str
989+
exit: str
990+
direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped
991+
percentage: int = 100
992+
993+
994+
PlandoConFromAnyType = typing.Union[
995+
typing.Iterable[typing.Union[typing.Mapping[str, typing.Any], PlandoConnection, typing.Any]], typing.Any
996+
]
997+
998+
999+
class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=ConnectionsMeta):
1000+
"""Generic connections plando. Format is:
1001+
- entrance: "Entrance Name"
1002+
exit: "Exit Name"
1003+
direction: "Direction"
1004+
percentage: 100
1005+
Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted.
1006+
Percentage is an integer from 1 to 100, and defaults to 100 when omitted."""
1007+
1008+
display_name = "Plando Connections"
1009+
1010+
default = ()
1011+
supports_weighting = False
1012+
1013+
entrances: typing.ClassVar[typing.AbstractSet[str]]
1014+
exits: typing.ClassVar[typing.AbstractSet[str]]
1015+
1016+
duplicate_exits: bool = False
1017+
"""Whether or not exits should be allowed to be duplicate."""
1018+
1019+
def __init__(self, value: typing.Iterable[PlandoConnection]):
1020+
self.value = list(deepcopy(value))
1021+
super(PlandoConnections, self).__init__()
1022+
1023+
@classmethod
1024+
def validate_entrance_name(cls, entrance: str) -> bool:
1025+
return entrance.lower() in cls.entrances
1026+
1027+
@classmethod
1028+
def validate_exit_name(cls, exit: str) -> bool:
1029+
return exit.lower() in cls.exits
1030+
1031+
@classmethod
1032+
def can_connect(cls, entrance: str, exit: str) -> bool:
1033+
"""Checks that a given entrance can connect to a given exit.
1034+
By default, this will always return true unless overridden."""
1035+
return True
1036+
1037+
@classmethod
1038+
def validate_plando_connections(cls, connections: typing.Iterable[PlandoConnection]) -> None:
1039+
used_entrances: typing.List[str] = []
1040+
used_exits: typing.List[str] = []
1041+
for connection in connections:
1042+
entrance = connection.entrance
1043+
exit = connection.exit
1044+
direction = connection.direction
1045+
if direction not in (PlandoConnection.Direction.entrance,
1046+
PlandoConnection.Direction.exit,
1047+
PlandoConnection.Direction.both):
1048+
raise ValueError(f"Unknown direction: {direction}")
1049+
if entrance in used_entrances:
1050+
raise ValueError(f"Duplicate Entrance {entrance} not allowed.")
1051+
if not cls.duplicate_exits and exit in used_exits:
1052+
raise ValueError(f"Duplicate Exit {exit} not allowed.")
1053+
used_entrances.append(entrance)
1054+
used_exits.append(exit)
1055+
if not cls.validate_entrance_name(entrance):
1056+
raise ValueError(f"{entrance.title()} is not a valid entrance.")
1057+
if not cls.validate_exit_name(exit):
1058+
raise ValueError(f"{exit.title()} is not a valid exit.")
1059+
if not cls.can_connect(entrance, exit):
1060+
raise ValueError(f"Connection between {entrance.title()} and {exit.title()} is invalid.")
1061+
1062+
@classmethod
1063+
def from_any(cls, data: PlandoConFromAnyType) -> Self:
1064+
if not isinstance(data, typing.Iterable):
1065+
raise Exception(f"Cannot create plando connections from non-List value, got {type(data)}.")
1066+
1067+
value: typing.List[PlandoConnection] = []
1068+
for connection in data:
1069+
if isinstance(connection, typing.Mapping):
1070+
percentage = connection.get("percentage", 100)
1071+
if random.random() < float(percentage / 100):
1072+
entrance = connection.get("entrance", None)
1073+
if is_iterable_except_str(entrance):
1074+
entrance = random.choice(sorted(entrance))
1075+
exit = connection.get("exit", None)
1076+
if is_iterable_except_str(exit):
1077+
exit = random.choice(sorted(exit))
1078+
direction = connection.get("direction", "both")
1079+
1080+
if not entrance or not exit:
1081+
raise Exception("Plando connection must have an entrance and an exit.")
1082+
value.append(PlandoConnection(
1083+
entrance,
1084+
exit,
1085+
direction,
1086+
percentage
1087+
))
1088+
elif isinstance(connection, PlandoConnection):
1089+
if random.random() < float(connection.percentage / 100):
1090+
value.append(connection)
1091+
else:
1092+
raise Exception(f"Cannot create connection from non-Dict type, got {type(connection)}.")
1093+
cls.validate_plando_connections(value)
1094+
return cls(value)
1095+
1096+
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
1097+
from BaseClasses import PlandoOptions
1098+
if self.value and not (PlandoOptions.connections & plando_options):
1099+
# plando is disabled but plando options were given so overwrite the options
1100+
self.value = []
1101+
logging.warning(f"The plando connections module is turned off, "
1102+
f"so connections for {player_name} will be ignored.")
1103+
1104+
@classmethod
1105+
def get_option_name(cls, value: typing.List[PlandoConnection]) -> str:
1106+
return ", ".join(["%s %s %s" % (connection.entrance,
1107+
"<=>" if connection.direction == PlandoConnection.Direction.both else
1108+
"<=" if connection.direction == PlandoConnection.Direction.exit else
1109+
"=>",
1110+
connection.exit) for connection in value])
1111+
1112+
def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection:
1113+
return self.value.__getitem__(index)
1114+
1115+
def __iter__(self) -> typing.Iterator[PlandoConnection]:
1116+
yield from self.value
1117+
1118+
def __len__(self) -> int:
1119+
return len(self.value)
1120+
1121+
8991122
class Accessibility(Choice):
9001123
"""Set rules for reachability of your items/locations.
9011124
Locations: ensure everything can be reached and acquired.
@@ -1049,7 +1272,8 @@ class ItemLinks(OptionList):
10491272
])
10501273

10511274
@staticmethod
1052-
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world, allow_item_groups: bool = True) -> typing.Set:
1275+
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world,
1276+
allow_item_groups: bool = True) -> typing.Set:
10531277
pool = set()
10541278
for item_name in items:
10551279
if item_name not in world.item_names and (not allow_item_groups or item_name not in world.item_name_groups):

worlds/alttp/Options.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import typing
22

33
from BaseClasses import MultiWorld
4-
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
5-
FreeText, Removed
4+
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \
5+
StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText, Removed
6+
from .EntranceShuffle import default_connections, default_dungeon_connections, \
7+
inverted_default_connections, inverted_default_dungeon_connections
8+
from .Text import TextTable
69

710

811
class GlitchesRequired(Choice):
@@ -721,7 +724,27 @@ class AllowCollect(DefaultOnToggle):
721724
display_name = "Allow Collection of checks for other players"
722725

723726

727+
class ALttPPlandoConnections(PlandoConnections):
728+
entrances = set([connection[0] for connection in (
729+
*default_connections, *default_dungeon_connections, *inverted_default_connections,
730+
*inverted_default_dungeon_connections)])
731+
exits = set([connection[1] for connection in (
732+
*default_connections, *default_dungeon_connections, *inverted_default_connections,
733+
*inverted_default_dungeon_connections)])
734+
735+
736+
class ALttPPlandoTexts(PlandoTexts):
737+
"""Text plando. Format is:
738+
- text: 'This is your text'
739+
at: text_key
740+
percentage: 100
741+
Percentage is an integer from 1 to 100, and defaults to 100 when omitted."""
742+
valid_keys = TextTable.valid_keys
743+
744+
724745
alttp_options: typing.Dict[str, type(Option)] = {
746+
"plando_connections": ALttPPlandoConnections,
747+
"plando_texts": ALttPPlandoTexts,
725748
"start_inventory_from_pool": StartInventoryPool,
726749
"goal": Goal,
727750
"mode": Mode,

worlds/alttp/Rom.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2538,12 +2538,12 @@ def hint_text(dest, ped_hint=False):
25382538
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
25392539
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
25402540

2541-
for at, text in world.plando_texts[player].items():
2541+
for at, text, _ in world.plando_texts[player]:
25422542

25432543
if at not in tt:
25442544
raise Exception(f"No text target \"{at}\" found.")
25452545
else:
2546-
tt[at] = text
2546+
tt[at] = "\n".join(text)
25472547

25482548
rom.write_bytes(0xE0000, tt.getBytes())
25492549

worlds/alttp/Shops.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99

1010
from BaseClasses import CollectionState
1111
from .SubClasses import ALttPLocation
12-
from .EntranceShuffle import door_addresses
12+
1313
from .Items import item_name_groups
14-
from .Options import small_key_shuffle, RandomizeShopInventories
14+
1515
from .StateHelpers import has_hearts, can_use_bombs, can_hold_arrows
1616

1717
logger = logging.getLogger("Shops")
@@ -66,6 +66,7 @@ def item_count(self) -> int:
6666
return 0
6767

6868
def get_bytes(self) -> List[int]:
69+
from .EntranceShuffle import door_addresses
6970
# [id][roomID-low][roomID-high][doorID][zero][shop_config][shopkeeper_config][sram_index]
7071
entrances = self.region.entrances
7172
config = self.item_count
@@ -181,7 +182,7 @@ def push_shop_inventories(multiworld):
181182

182183

183184
def create_shops(multiworld, player: int):
184-
185+
from .Options import RandomizeShopInventories
185186
player_shop_table = shop_table.copy()
186187
if multiworld.include_witch_hut[player]:
187188
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
@@ -304,6 +305,7 @@ class ShopData(NamedTuple):
304305

305306

306307
def set_up_shops(multiworld, player: int):
308+
from .Options import small_key_shuffle
307309
# TODO: move hard+ mode changes for shields here, utilizing the new shops
308310

309311
if multiworld.retro_bow[player]:
@@ -426,7 +428,7 @@ def get_price_modifier(item):
426428

427429
def get_price(multiworld, item, player: int, price_type=None):
428430
"""Converts a raw Rupee price into a special price type"""
429-
431+
from .Options import small_key_shuffle
430432
if price_type:
431433
price_types = [price_type]
432434
else:

0 commit comments

Comments
 (0)