Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: move PlandoConnections and PlandoTexts to the options system #2904

Merged
merged 59 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
c2be561
initial commit, works but need to port games
Silvris Nov 25, 2023
aab79bb
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Jan 2, 2024
9ec268d
port lttp and minecraft
Silvris Jan 3, 2024
226932e
rewrite can_connect to return true by default
Silvris Jan 3, 2024
1f43d4d
text support + update to always use entrance/exit
Silvris Feb 4, 2024
913a49a
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Mar 5, 2024
fea2a34
remove PlandoConnection stub, finish texts support
Silvris Mar 5, 2024
184129a
actually error check direction
Silvris Mar 5, 2024
6fee5a2
Update __init__.py
Silvris Mar 5, 2024
5571fc5
remove now unused imports
Silvris Mar 5, 2024
51cda33
fix some issues
beauxq Mar 5, 2024
19b9c03
Merge pull request #4 from beauxq/silvris-plando_options
Silvris Mar 6, 2024
7b69879
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Mar 6, 2024
1b88395
support KDL3
Silvris Mar 6, 2024
65bc9bf
Update Options.py
Silvris Mar 6, 2024
6a3b982
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Mar 15, 2024
e0bf453
support tunic plando connections
Silvris Mar 15, 2024
ed582ae
forgot the option
Silvris Mar 15, 2024
4cf6616
missed cases
Silvris Mar 15, 2024
c8a0e53
scope Direction under PlandoConnection
Silvris Mar 15, 2024
a76b008
invert first if, fix missed Direction reference
Silvris Mar 15, 2024
7859129
swap to options API for plando connections
alwaysintreble Mar 15, 2024
194de1b
fix a bunch of bugs
alwaysintreble Mar 15, 2024
8d3366f
only do plando when setting is enabled
alwaysintreble Mar 15, 2024
3ca1179
Merge pull request #5 from alwaysintreble/messenger_plando_api
Silvris Mar 16, 2024
3fb3c3e
fix bad unused import
Silvris Mar 16, 2024
8086f0b
Merge branch 'main' into plando_options
Silvris Mar 22, 2024
b840d15
Merge branch 'main' into plando_options
Silvris Mar 24, 2024
a0d3002
Apply suggestions from code review
Silvris Mar 24, 2024
b0f5dea
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Apr 3, 2024
9737715
fix LttP circular imports
Silvris Apr 4, 2024
aa894a0
allow passing lists into entrance and exit
Silvris Apr 4, 2024
4c5f409
fix log warnings when option is empty but extant
Silvris Apr 4, 2024
0ad6af1
Merge branch 'main' into plando_options
Silvris Apr 14, 2024
39284e1
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Apr 16, 2024
1e4a2f8
change plando text to use list of str for text
Silvris Apr 16, 2024
0591b2c
Update worlds/tunic/options.py
Silvris Apr 16, 2024
9ae1db0
Update worlds/tunic/options.py
Silvris Apr 16, 2024
8bbbd86
Merge branch 'main' into plando_options
Silvris Apr 16, 2024
a96c57f
wrap in sorted
Silvris Apr 21, 2024
d3fc611
Merge remote-tracking branch 'upstream/main' into plando_options
Silvris Apr 21, 2024
b27d71d
Merge branch 'main' into plando_options
Silvris Apr 30, 2024
3a1bcff
The Messenger: fix portal plando
alwaysintreble May 3, 2024
2ad1270
The Messenger: rename portal plando so that transition plando can be …
alwaysintreble May 3, 2024
ddc8b00
fix circular import
alwaysintreble May 3, 2024
f5c2dc4
won't let me inherit entrances and exits :(
alwaysintreble May 3, 2024
af84aef
Merge pull request #6 from alwaysintreble/fix_plando_portal
Silvris May 4, 2024
ea37f2a
Merge branch 'main' into plando_options
Silvris May 5, 2024
9a2593a
Update __init__.py
Silvris May 5, 2024
e6eac0c
allow duplicate exits, apply to TUNIC
Silvris May 11, 2024
3d2bde4
Update Options.py
Silvris May 13, 2024
22e4d59
Merge branch 'main' into plando_options
Silvris May 13, 2024
3b1dbd9
update typing on direction with todo
Silvris May 14, 2024
6af6ba2
Merge branch 'main' into plando_options
Silvris May 15, 2024
d3dfb96
fix incorrect can_connect
Silvris May 16, 2024
1edf701
Merge branch 'main' into plando_options
Silvris May 20, 2024
8fad93b
Merge branch 'main' into plando_options
Silvris May 29, 2024
2991ea2
adjust tunic shop handling
Silvris May 31, 2024
a18e668
Merge branch 'main' into plando_options
Silvris May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 2 additions & 28 deletions Generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,8 @@
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 worlds.generic import PlandoConnection


def mystery_argparse():
Expand Down Expand Up @@ -465,35 +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)
if PlandoOptions.connections in plando_options:
ret.plando_connections = []
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, "both")
))
roll_alttp_settings(ret, game_weights)

return ret


def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):

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))

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:
Expand Down
215 changes: 214 additions & 1 deletion Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -900,6 +901,217 @@ class ItemSet(OptionSet):
convert_name_groups = True


class PlandoText(typing.NamedTuple):
at: str
text: str
Silvris marked this conversation as resolved.
Show resolved Hide resolved
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 = ()
supports_weighting = False
display_name = "Plando Texts"

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:
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: PlandoTextsFromAnyType) -> Self:
texts: typing.List[PlandoText] = []
if isinstance(data, typing.Iterable):
Silvris marked this conversation as resolved.
Show resolved Hide resolved
for text in data:
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("percentage", 100)
))
elif isinstance(text, PlandoText):
if random.random() < float(text.percentage/100):
Silvris marked this conversation as resolved.
Show resolved Hide resolved
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])
return cls(texts)
else:
raise NotImplementedError(f"Cannot Convert from non-list, got {type(data)}")

@classmethod
def get_option_name(cls, value: typing.List[PlandoText]) -> str:
return str({text.at: text.text for text in value})

def __iter__(self) -> typing.Iterator[PlandoText]:
yield from self.value

def __getitem__(self, index: typing.SupportsIndex) -> PlandoText:
return self.value.__getitem__(index)

def __len__(self) -> int:
return self.value.__len__()


class ConnectionsMeta(AssembleOptions):
def __new__(mcs, name, bases, attrs):
Silvris marked this conversation as resolved.
Show resolved Hide resolved
if name != "PlandoConnections":
Silvris marked this conversation as resolved.
Show resolved Hide resolved
Silvris marked this conversation as resolved.
Show resolved Hide resolved
Silvris marked this conversation as resolved.
Show resolved Hide resolved
Silvris marked this conversation as resolved.
Show resolved Hide resolved
Silvris marked this conversation as resolved.
Show resolved Hide resolved
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


class PlandoConnection(typing.NamedTuple):
entrance: str
exit: str
direction: str # entrance, exit or both
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"
exit: "Exit Name"
direction: "Direction"
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is going to be a scoped class like this it should probably be in the PlandoConnection one instead of here so that the direction can be correctly typed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this.

But also, maybe an Enum?

(The downside to making it an Enum is the long definition... PlandoConnection.Direction.both compared to "both")

So another alternative is no class, just a type alias:

PlandoConnectionDirection = typing.Literal["entrance", "exit", "both"]


class PlandoConnection(typing.NamedTuple):
    entrance: str
    exit: str
    direction: PlandoConnectionDirection
    percentage: int = 100

(I think a type alias has to be gloabal scope.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once 3.8 is dropped, StrEnum would probably be a good option. Not sure what the best option is for right now.

Copy link
Collaborator

@beauxq beauxq Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see what value StrEnum would have over Enum for this.
The values in a normal Enum can be str.
StrEnum is for when you want to pass it to a function that only takes str, or use str methods like lower, but I don't see you doing any of that here.

Copy link
Collaborator

@beauxq beauxq Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It almost seems better to make the values:

class Direction(Enum):
    ENTRANCE = "=>"
    EXIT = "<="
    BOTH = "<=>"

so that you could use those values in get_option_name (and simplify the code there)

Copy link
Collaborator

@alwaysintreble alwaysintreble Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue with that is that the existing implementations specifically compare the string when resolving the connections, so it'd need to be an alias of some sort.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I didn't realize this was connecting with past implementations.

Entrance = "entrance"
Exit = "exit"
Both = "both"

display_name = "Plando Connections"

default = ()
supports_weighting = False

entrances: typing.ClassVar[typing.AbstractSet[str]]
exits: typing.ClassVar[typing.AbstractSet[str]]

def __init__(self, value: typing.Iterable[PlandoConnection]):
self.value = list(deepcopy(value))
super(PlandoConnections, self).__init__()

@classmethod
def validate_entrance_name(cls, entrance: str) -> bool:
return entrance.lower() in cls.entrances

@classmethod
def validate_exit_name(cls, exit: str) -> bool:
return exit.lower() in cls.exits

@classmethod
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure this should be the default behavior? it seems like enough of the worlds that use this would be fine with it (and my world is too) but it almost feels backwards.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It primarily does two things.

As you've said, it's sufficient for worlds that don't have wild constraints on possible connections. It also simplifies backward compatibility with the currently implemented games, as they already have to check for whether or not the plando is a valid connection, so we can skip the option-based check (the exception being Minecraft, as it's can_connect was simple enough to implement on the option itself).


@classmethod
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.")
Silvris marked this conversation as resolved.
Show resolved Hide resolved
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: 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):
Silvris marked this conversation as resolved.
Show resolved Hide resolved
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.")

@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
"=>",
connection.exit) for connection in value])

def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection:
return self.value.__getitem__(index)

def __iter__(self) -> typing.Iterator[PlandoConnection]:
yield from self.value

def __len__(self) -> int:
return len(self.value)


class Accessibility(Choice):
"""Set rules for reachability of your items/locations.
Locations: ensure everything can be reached and acquired.
Expand Down Expand Up @@ -1051,7 +1263,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):
Expand Down
27 changes: 25 additions & 2 deletions worlds/alttp/Options.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import typing

from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses,\
FreeText
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, \
StartInventoryPool, PlandoBosses, PlandoConnections, PlandoTexts, FreeText
from .EntranceShuffle import default_connections, default_dungeon_connections, \
inverted_default_connections, inverted_default_dungeon_connections
from .Text import TextTable


class GlitchesRequired(Choice):
Expand Down Expand Up @@ -722,7 +725,27 @@ 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)])


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,
"start_inventory_from_pool": StartInventoryPool,
"goal": Goal,
"mode": Mode,
Expand Down
2 changes: 1 addition & 1 deletion worlds/alttp/Rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
Loading
Loading