2020-03-18 15:15:32 +00:00
|
|
|
from __future__ import annotations
|
2023-07-22 00:31:23 +00:00
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
import abc
|
2023-10-10 20:30:20 +00:00
|
|
|
import functools
|
2024-03-03 21:30:51 +00:00
|
|
|
import logging
|
2022-04-07 17:42:30 +00:00
|
|
|
import math
|
|
|
|
import numbers
|
2021-06-08 12:15:23 +00:00
|
|
|
import random
|
2023-07-22 00:31:23 +00:00
|
|
|
import typing
|
2024-04-14 18:49:43 +00:00
|
|
|
import enum
|
2023-07-22 00:31:23 +00:00
|
|
|
from copy import deepcopy
|
2024-03-03 21:30:51 +00:00
|
|
|
from dataclasses import dataclass
|
2023-07-22 00:31:23 +00:00
|
|
|
|
|
|
|
from schema import And, Optional, Or, Schema
|
2024-06-01 11:34:41 +00:00
|
|
|
from typing_extensions import Self
|
2020-03-18 15:15:32 +00:00
|
|
|
|
2024-03-11 23:30:14 +00:00
|
|
|
from Utils import get_fuzzy_results, is_iterable_except_str
|
2022-02-05 14:49:19 +00:00
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
if typing.TYPE_CHECKING:
|
|
|
|
from BaseClasses import PlandoOptions
|
|
|
|
from worlds.AutoWorld import World
|
2023-04-15 23:57:52 +00:00
|
|
|
import pathlib
|
2023-03-07 07:44:20 +00:00
|
|
|
|
2022-05-21 18:57:26 +00:00
|
|
|
|
2024-05-02 07:22:50 +00:00
|
|
|
class OptionError(ValueError):
|
|
|
|
pass
|
|
|
|
|
WebHost: Massive overhaul of options pages (#2614)
* Implement support for option groups. WebHost options pages still need to be updated.
* Remove debug output
* In-progress conversion of player-options to Jinja rendering
* Support "Randomize" button without JS, transpile SCSS to CSS, include map file for later editors
* Un-alphabetize options, add default group name for item/location Option classes, implement more option types
* Re-flow UI generation to avoid printing rows with unsupported or invalid option types, add support for TextChoice options
* Support all remaining option types
* Rendering improvements and CSS fixes for prettiness
* Wrap options in a form, update button styles, fix labels, disable inputs where the default is random, nuke the JS
* Minor CSS tweaks, as recommended by the designer
* Hide JS-required elements in noscript tag. Add JS reactivity to range, named-range, and randomize buttons.
* Fix labels, add JS handling for TextChoice
* Make option groups collapsable
* PEP8 current option_groups progress (#2604)
* Make the python more PEP8 and remove unneeded imports
* remove LocationSet from `Item & Location Options` group
* It's ugly, but YAML generation is working
* Stop generating JSON files for player-options pages
* Do not include ItemDict entries whose values are zero
* Properly format yaml output
* Save options when form is submitted, load options on page load
* Fix options being omitted from the page if a group has an even number of options
* Implement generate-game, escape option descriptions
* Fix "randomize" checkboxes not properly setting YAML options to "random"
* Add a separator between item/location groups and items/locations in their respective lists
* Implement option presets
* Fix docs to detail what actually ended up happening
* implement option groups on webworld to allow dev sorting (#2616)
* Force extremely long item/location/option names with no spaces to text-wrap
* Fix "randomize" button being too wide in single-column display, change page header to include game name
* Update preset select to read "custom" when updating form inputs. Show error message if the user doesn't input a name
* Un-break weighted-options, add option group names to weighted options
* Nuke weighted-options. Set up framework to rebuild it in Jinja.
* Generate styles with scss, remove styles which will be replaced, add placeholders for worlds
* Support Toggle, DefaultOnToggle, and Choice options in weighted-options
* Implement expand/collapse without JS for worlds and option groups
* Properly style set options
* Implement Range and NamedRange. Also, CSS is hard.
* Add support for remaining option types. JS and backend still forthcoming.
* Add JS functionality for collapsing game divs, populating span values on range updates. Add <noscript> tag to warn users with JS disabled.
* Support showing/hiding game divs based on range value for game
* Add support for adding/deleting range rows
* Save settings to localStorage on form submission
* Save deleted options on form submission
* Break weighted-options into a per-game page.
- Break weighted-options into a per-game page
- Add "advanced options" links to supported games page
- Use details/summary tags on supported games, player-options, and weighted-options
- Fix bug preventing previously deleted rows from being removed on page load if JS is enabled
- Move route handling for options pages to options.py
- Remove world handling from weighted-options
* Implement loading previous settings from localStorage on page load if JS is enabled
* Weighted options can now generate YAML files and single-player games
* options pages now respect option visibility settings for simple and complex pages
* Remove `/weighted-settings` redirect, fix weighted-options link on player-options page
* Fix instance of AutoWorld not having access to proper `random`
* Catch instances of frozenset along with set
* Restore word-wrap in tooltips
* Fix word wrap in player-options labels
* Add `dedent` filter to help with formatting tooltips in player-options
* Do not change the ordering of keys when printing yaml files
* Move necessary import out of conditional statement
* Expand only the first option group by default on both options pages
* Respect option visibility when generating yaml template files
* Swap to double quotes
* Replace instances of `/weighted-settings` with `/weighted-options`, swap out incomplete links
* Strip newlines and spaces after applying dedent filter
* Fix documentation for option groups
* Update site map
* Update various docs
* Sort OptionSet lists alphabetically
* Minor style tweak
* Fix extremely long text overflowing tooltips
* Convert player-options to use CSS grid instead of tables
* Do not display link to weighted-options page on supported games if the options page is an external link
* Update worlds/AutoWorld.py
Bugfix by @alwaysintreble
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
* Fix NamedRange options not being properly set if a preset it loaded
* Move option-presets route into options.py
* Include preset name in YAML if not "default" and not "custom"
* Removed macros for PlandoBosses and DefaultOnToggle, as they were handled by their parent classes
* Fix not disabling custom inputs when the randomize button is clicked
* Only sort OptionList and OptionSet valid_keys if they are unordered
* Quick style fixes for player-settings to give `select` elements `text-overflow: ellipsis` and increase base size of left-column
* Prevent showing a horizontal scroll bar on player-options if the browser width was beneath a certain threshold
* Fix a bug in weighted-options which prevented inputting a negative value for new range inputs
---------
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-05-18 04:11:57 +00:00
|
|
|
|
2024-04-14 18:49:43 +00:00
|
|
|
class Visibility(enum.IntFlag):
|
|
|
|
none = 0b0000
|
|
|
|
template = 0b0001
|
|
|
|
simple_ui = 0b0010 # show option in simple menus, such as player-options
|
|
|
|
complex_ui = 0b0100 # show option in complex menus, such as weighted-options
|
|
|
|
spoiler = 0b1000
|
|
|
|
all = 0b1111
|
|
|
|
|
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
class AssembleOptions(abc.ABCMeta):
|
2021-04-14 15:51:11 +00:00
|
|
|
def __new__(mcs, name, bases, attrs):
|
2020-10-24 04:43:35 +00:00
|
|
|
options = attrs["options"] = {}
|
|
|
|
name_lookup = attrs["name_lookup"] = {}
|
2021-08-03 17:03:41 +00:00
|
|
|
# merge parent class options
|
2020-10-24 04:43:35 +00:00
|
|
|
for base in bases:
|
2021-08-30 14:31:56 +00:00
|
|
|
if getattr(base, "options", None):
|
2021-06-08 19:58:11 +00:00
|
|
|
options.update(base.options)
|
2021-08-09 07:15:41 +00:00
|
|
|
name_lookup.update(base.name_lookup)
|
2020-10-24 04:43:35 +00:00
|
|
|
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
|
2021-03-20 23:47:17 +00:00
|
|
|
name.startswith("option_")}
|
2022-03-23 01:28:15 +00:00
|
|
|
|
2022-03-26 00:12:54 +00:00
|
|
|
assert "random" not in new_options, "Choice option 'random' cannot be manually assigned."
|
|
|
|
assert len(new_options) == len(set(new_options.values())), "same ID cannot be used twice. Try alias?"
|
2022-03-23 01:28:15 +00:00
|
|
|
|
2020-10-24 04:43:35 +00:00
|
|
|
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
|
|
|
|
options.update(new_options)
|
2021-03-20 23:47:17 +00:00
|
|
|
# apply aliases, without name_lookup
|
2024-06-15 02:50:26 +00:00
|
|
|
aliases = attrs["aliases"] = {name[6:].lower(): option_id for name, option_id in attrs.items() if
|
|
|
|
name.startswith("alias_")}
|
2022-06-10 20:24:44 +00:00
|
|
|
|
2024-03-12 21:03:57 +00:00
|
|
|
assert (
|
|
|
|
name in {"Option", "VerifyKeys"} or # base abstract classes don't need default
|
|
|
|
"default" in attrs or
|
|
|
|
any(hasattr(base, "default") for base in bases)
|
|
|
|
), f"Option class {name} needs default value"
|
2022-06-10 20:24:44 +00:00
|
|
|
assert "random" not in aliases, "Choice option 'random' cannot be manually assigned."
|
|
|
|
|
2022-09-15 22:32:30 +00:00
|
|
|
# auto-alias Off and On being parsed as True and False
|
|
|
|
if "off" in options:
|
|
|
|
options["false"] = options["off"]
|
|
|
|
if "on" in options:
|
|
|
|
options["true"] = options["on"]
|
|
|
|
|
2022-06-10 20:24:44 +00:00
|
|
|
options.update(aliases)
|
2021-07-18 23:02:23 +00:00
|
|
|
|
2022-09-17 00:55:33 +00:00
|
|
|
if "verify" not in attrs:
|
|
|
|
# not overridden by class -> look up bases
|
|
|
|
verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f]
|
|
|
|
if len(verifiers) > 1: # verify multiple bases/mixins
|
|
|
|
def verify(self, *args, **kwargs) -> None:
|
|
|
|
for f in verifiers:
|
|
|
|
f(self, *args, **kwargs)
|
2024-03-03 21:30:51 +00:00
|
|
|
|
2022-09-17 00:55:33 +00:00
|
|
|
attrs["verify"] = verify
|
|
|
|
else:
|
|
|
|
assert verifiers, "class Option is supposed to implement def verify"
|
|
|
|
|
2021-07-18 23:02:23 +00:00
|
|
|
# auto-validate schema on __init__
|
|
|
|
if "schema" in attrs.keys():
|
2022-02-05 14:49:19 +00:00
|
|
|
|
|
|
|
if "__init__" in attrs:
|
|
|
|
def validate_decorator(func):
|
|
|
|
def validate(self, *args, **kwargs):
|
|
|
|
ret = func(self, *args, **kwargs)
|
|
|
|
self.value = self.schema.validate(self.value)
|
|
|
|
return ret
|
|
|
|
|
|
|
|
return validate
|
2022-04-01 01:42:56 +00:00
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
attrs["__init__"] = validate_decorator(attrs["__init__"])
|
|
|
|
else:
|
|
|
|
# construct an __init__ that calls parent __init__
|
|
|
|
|
|
|
|
cls = super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
|
|
|
|
|
|
|
def meta__init__(self, *args, **kwargs):
|
|
|
|
super(cls, self).__init__(*args, **kwargs)
|
2021-07-18 23:02:23 +00:00
|
|
|
self.value = self.schema.validate(self.value)
|
2021-09-30 17:49:36 +00:00
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
cls.__init__ = meta__init__
|
|
|
|
return cls
|
2021-09-30 17:49:36 +00:00
|
|
|
|
2021-04-14 15:51:11 +00:00
|
|
|
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
|
2020-10-24 04:43:35 +00:00
|
|
|
|
2022-04-01 01:42:56 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
T = typing.TypeVar('T')
|
2021-08-03 17:03:41 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
|
|
|
|
class Option(typing.Generic[T], metaclass=AssembleOptions):
|
|
|
|
value: T
|
2024-03-12 21:03:57 +00:00
|
|
|
default: typing.ClassVar[typing.Any] # something that __init__ will be able to convert to the correct type
|
2024-04-14 18:49:43 +00:00
|
|
|
visibility = Visibility.all
|
2020-10-24 04:43:35 +00:00
|
|
|
|
2022-02-02 15:29:29 +00:00
|
|
|
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
|
2021-08-03 17:03:41 +00:00
|
|
|
# Handled in get_option_name()
|
2022-02-02 15:29:29 +00:00
|
|
|
auto_display_name = False
|
2021-08-03 17:03:41 +00:00
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
# can be weighted between selections
|
|
|
|
supports_weighting = True
|
|
|
|
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc: typing.Optional[bool] = None
|
|
|
|
"""Whether the WebHost should render the Option's docstring as rich text.
|
|
|
|
|
|
|
|
If this is True, the Option's docstring is interpreted as reStructuredText_,
|
|
|
|
the standard Python markup format. In the WebHost, it's rendered to HTML so
|
|
|
|
that lists, emphasis, and other rich text features are displayed properly.
|
|
|
|
|
|
|
|
If this is False, the docstring is instead interpreted as plain text, and
|
|
|
|
displayed as-is on the WebHost with whitespace preserved.
|
|
|
|
|
|
|
|
If this is None, it inherits the value of `World.rich_text_options_doc`. For
|
|
|
|
backwards compatibility, this defaults to False, but worlds are encouraged to
|
|
|
|
set it to True and use reStructuredText for their Option documentation.
|
|
|
|
|
|
|
|
.. _reStructuredText: https://docutils.sourceforge.io/rst.html
|
|
|
|
"""
|
|
|
|
|
2022-03-31 22:47:50 +00:00
|
|
|
# filled by AssembleOptions:
|
2024-03-12 21:03:57 +00:00
|
|
|
name_lookup: typing.ClassVar[typing.Dict[T, str]] # type: ignore
|
|
|
|
# https://github.com/python/typing/discussions/1460 the reason for this type: ignore
|
|
|
|
options: typing.ClassVar[typing.Dict[str, int]]
|
2024-06-15 02:50:26 +00:00
|
|
|
aliases: typing.ClassVar[typing.Dict[str, int]]
|
2022-03-31 22:47:50 +00:00
|
|
|
|
2021-08-03 17:03:41 +00:00
|
|
|
def __repr__(self) -> str:
|
2023-03-07 07:44:20 +00:00
|
|
|
return f"{self.__class__.__name__}({self.current_option_name})"
|
2020-10-24 04:43:35 +00:00
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
def __hash__(self) -> int:
|
2020-10-24 04:43:35 +00:00
|
|
|
return hash(self.value)
|
|
|
|
|
2021-08-09 07:15:41 +00:00
|
|
|
@property
|
|
|
|
def current_key(self) -> str:
|
|
|
|
return self.name_lookup[self.value]
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
@property
|
|
|
|
def current_option_name(self) -> str:
|
|
|
|
"""For display purposes. Worlds should be using current_key."""
|
2021-08-03 17:09:37 +00:00
|
|
|
return self.get_option_name(self.value)
|
|
|
|
|
2021-08-31 20:14:18 +00:00
|
|
|
@classmethod
|
2022-03-31 22:47:50 +00:00
|
|
|
def get_option_name(cls, value: T) -> str:
|
2022-02-02 15:29:29 +00:00
|
|
|
if cls.auto_display_name:
|
2021-08-31 20:14:18 +00:00
|
|
|
return cls.name_lookup[value].replace("_", " ").title()
|
2021-08-03 17:03:41 +00:00
|
|
|
else:
|
2021-08-31 20:14:18 +00:00
|
|
|
return cls.name_lookup[value]
|
2020-10-24 04:43:35 +00:00
|
|
|
|
2022-03-31 22:47:50 +00:00
|
|
|
def __int__(self) -> T:
|
2020-10-24 04:43:35 +00:00
|
|
|
return self.value
|
|
|
|
|
2021-08-03 17:03:41 +00:00
|
|
|
def __bool__(self) -> bool:
|
2020-10-24 04:43:35 +00:00
|
|
|
return bool(self.value)
|
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
@classmethod
|
2023-01-31 20:26:09 +00:00
|
|
|
@abc.abstractmethod
|
2022-04-07 17:42:30 +00:00
|
|
|
def from_any(cls, data: typing.Any) -> Option[T]:
|
2023-01-31 20:26:09 +00:00
|
|
|
...
|
2021-03-14 07:38:02 +00:00
|
|
|
|
2022-09-17 00:55:33 +00:00
|
|
|
if typing.TYPE_CHECKING:
|
2023-03-07 07:44:20 +00:00
|
|
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
2022-09-17 00:55:33 +00:00
|
|
|
pass
|
|
|
|
else:
|
|
|
|
def verify(self, *args, **kwargs) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
class FreeText(Option[str]):
|
2022-09-17 00:55:33 +00:00
|
|
|
"""Text option that allows users to enter strings.
|
|
|
|
Needs to be validated by the world or option definition."""
|
|
|
|
|
2024-03-12 21:03:57 +00:00
|
|
|
default = ""
|
|
|
|
|
2022-09-17 00:55:33 +00:00
|
|
|
def __init__(self, value: str):
|
|
|
|
assert isinstance(value, str), "value of FreeText must be a string"
|
|
|
|
self.value = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def current_key(self) -> str:
|
|
|
|
return self.value
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str) -> FreeText:
|
|
|
|
return cls(text)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: typing.Any) -> FreeText:
|
|
|
|
return cls.from_text(str(data))
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-07 07:44:20 +00:00
|
|
|
def get_option_name(cls, value: str) -> str:
|
2022-09-17 00:55:33 +00:00
|
|
|
return value
|
|
|
|
|
2024-03-12 19:40:16 +00:00
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
return other.value == self.value
|
|
|
|
elif isinstance(other, str):
|
|
|
|
return other == self.value
|
|
|
|
else:
|
|
|
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
|
|
|
|
2020-10-24 04:43:35 +00:00
|
|
|
|
2023-01-31 20:26:09 +00:00
|
|
|
class NumericOption(Option[int], numbers.Integral, abc.ABC):
|
2022-09-28 21:54:10 +00:00
|
|
|
default = 0
|
2024-03-03 21:30:51 +00:00
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
# 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)
|
|
|
|
# https://github.com/python/typing/issues/272
|
|
|
|
# https://github.com/python/mypy/issues/3186
|
|
|
|
# https://github.com/microsoft/pyright/issues/1575
|
|
|
|
|
|
|
|
def __eq__(self, other: typing.Any) -> bool:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value == other.value
|
|
|
|
else:
|
|
|
|
return typing.cast(bool, self.value == other)
|
|
|
|
|
|
|
|
def __lt__(self, other: typing.Union[int, NumericOption]) -> bool:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value < other.value
|
|
|
|
else:
|
|
|
|
return self.value < other
|
|
|
|
|
|
|
|
def __le__(self, other: typing.Union[int, NumericOption]) -> bool:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value <= other.value
|
|
|
|
else:
|
|
|
|
return self.value <= other
|
|
|
|
|
|
|
|
def __gt__(self, other: typing.Union[int, NumericOption]) -> bool:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value > other.value
|
|
|
|
else:
|
|
|
|
return self.value > other
|
|
|
|
|
2023-10-10 20:30:20 +00:00
|
|
|
def __ge__(self, other: typing.Union[int, NumericOption]) -> bool:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value >= other.value
|
|
|
|
else:
|
|
|
|
return self.value >= other
|
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
def __bool__(self) -> bool:
|
|
|
|
return bool(self.value)
|
|
|
|
|
|
|
|
def __int__(self) -> int:
|
|
|
|
return self.value
|
|
|
|
|
|
|
|
def __mul__(self, other: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value * other.value
|
|
|
|
else:
|
|
|
|
return self.value * other
|
|
|
|
|
|
|
|
def __rmul__(self, other: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return other.value * self.value
|
|
|
|
else:
|
|
|
|
return other * self.value
|
|
|
|
|
|
|
|
def __sub__(self, other: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value - other.value
|
|
|
|
else:
|
|
|
|
return self.value - other
|
|
|
|
|
|
|
|
def __rsub__(self, left: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(left, NumericOption):
|
|
|
|
return left.value - self.value
|
|
|
|
else:
|
|
|
|
return left - self.value
|
|
|
|
|
|
|
|
def __add__(self, other: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value + other.value
|
|
|
|
else:
|
|
|
|
return self.value + other
|
|
|
|
|
|
|
|
def __radd__(self, left: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(left, NumericOption):
|
|
|
|
return left.value + self.value
|
|
|
|
else:
|
|
|
|
return left + self.value
|
|
|
|
|
|
|
|
def __truediv__(self, other: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(other, NumericOption):
|
|
|
|
return self.value / other.value
|
|
|
|
else:
|
|
|
|
return self.value / other
|
|
|
|
|
|
|
|
def __rtruediv__(self, left: typing.Any) -> typing.Any:
|
|
|
|
if isinstance(left, NumericOption):
|
|
|
|
return left.value / self.value
|
|
|
|
else:
|
|
|
|
return left / self.value
|
|
|
|
|
|
|
|
def __abs__(self) -> typing.Any:
|
|
|
|
return abs(self.value)
|
|
|
|
|
|
|
|
def __and__(self, other: typing.Any) -> int:
|
|
|
|
return self.value & int(other)
|
|
|
|
|
|
|
|
def __ceil__(self) -> int:
|
|
|
|
return math.ceil(self.value)
|
|
|
|
|
|
|
|
def __floor__(self) -> int:
|
|
|
|
return math.floor(self.value)
|
|
|
|
|
|
|
|
def __floordiv__(self, other: typing.Any) -> int:
|
|
|
|
return self.value // int(other)
|
|
|
|
|
|
|
|
def __invert__(self) -> int:
|
|
|
|
return ~(self.value)
|
|
|
|
|
|
|
|
def __lshift__(self, other: typing.Any) -> int:
|
|
|
|
return self.value << int(other)
|
|
|
|
|
|
|
|
def __mod__(self, other: typing.Any) -> int:
|
|
|
|
return self.value % int(other)
|
|
|
|
|
|
|
|
def __neg__(self) -> int:
|
|
|
|
return -(self.value)
|
|
|
|
|
|
|
|
def __or__(self, other: typing.Any) -> int:
|
|
|
|
return self.value | int(other)
|
|
|
|
|
|
|
|
def __pos__(self) -> int:
|
|
|
|
return +(self.value)
|
|
|
|
|
|
|
|
def __pow__(self, exponent: numbers.Complex, modulus: typing.Optional[numbers.Integral] = None) -> int:
|
|
|
|
if not (modulus is None):
|
|
|
|
assert isinstance(exponent, numbers.Integral)
|
|
|
|
return pow(self.value, exponent, modulus) # type: ignore
|
|
|
|
return self.value ** exponent # type: ignore
|
|
|
|
|
|
|
|
def __rand__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) & self.value
|
|
|
|
|
|
|
|
def __rfloordiv__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) // self.value
|
|
|
|
|
|
|
|
def __rlshift__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) << self.value
|
|
|
|
|
|
|
|
def __rmod__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) % self.value
|
|
|
|
|
|
|
|
def __ror__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) | self.value
|
|
|
|
|
|
|
|
def __round__(self, ndigits: typing.Optional[int] = None) -> int:
|
|
|
|
return round(self.value, ndigits)
|
|
|
|
|
|
|
|
def __rpow__(self, base: typing.Any) -> typing.Any:
|
|
|
|
return base ** self.value
|
|
|
|
|
|
|
|
def __rrshift__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) >> self.value
|
|
|
|
|
|
|
|
def __rshift__(self, other: typing.Any) -> int:
|
|
|
|
return self.value >> int(other)
|
|
|
|
|
|
|
|
def __rxor__(self, other: typing.Any) -> int:
|
|
|
|
return int(other) ^ self.value
|
|
|
|
|
|
|
|
def __trunc__(self) -> int:
|
|
|
|
return math.trunc(self.value)
|
|
|
|
|
|
|
|
def __xor__(self, other: typing.Any) -> int:
|
|
|
|
return self.value ^ int(other)
|
|
|
|
|
|
|
|
|
|
|
|
class Toggle(NumericOption):
|
2020-10-24 04:43:35 +00:00
|
|
|
option_false = 0
|
|
|
|
option_true = 1
|
2021-04-03 12:47:49 +00:00
|
|
|
default = 0
|
2020-10-24 04:43:35 +00:00
|
|
|
|
|
|
|
def __init__(self, value: int):
|
2024-04-21 01:37:28 +00:00
|
|
|
# if user puts in an invalid value, make it valid
|
|
|
|
value = int(bool(value))
|
2020-10-24 04:43:35 +00:00
|
|
|
self.value = value
|
2020-03-18 15:15:32 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str) -> Toggle:
|
2022-03-31 20:09:54 +00:00
|
|
|
if text == "random":
|
|
|
|
return cls(random.choice(list(cls.name_lookup)))
|
|
|
|
elif text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
2020-10-24 04:43:35 +00:00
|
|
|
return cls(0)
|
|
|
|
else:
|
|
|
|
return cls(1)
|
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: typing.Any):
|
|
|
|
if type(data) == str:
|
|
|
|
return cls.from_text(data)
|
|
|
|
else:
|
2022-08-27 07:21:47 +00:00
|
|
|
return cls(int(data))
|
2021-03-14 07:38:02 +00:00
|
|
|
|
2021-08-31 20:52:14 +00:00
|
|
|
@classmethod
|
|
|
|
def get_option_name(cls, value):
|
2021-08-03 17:09:37 +00:00
|
|
|
return ["No", "Yes"][int(value)]
|
2020-10-24 04:43:35 +00:00
|
|
|
|
2022-01-06 05:18:54 +00:00
|
|
|
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
|
2021-06-18 23:00:21 +00:00
|
|
|
class DefaultOnToggle(Toggle):
|
|
|
|
default = 1
|
2021-04-03 12:47:49 +00:00
|
|
|
|
2021-08-03 17:03:41 +00:00
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
class Choice(NumericOption):
|
2022-02-02 15:29:29 +00:00
|
|
|
auto_display_name = True
|
2021-08-03 17:03:41 +00:00
|
|
|
|
2020-10-24 04:43:35 +00:00
|
|
|
def __init__(self, value: int):
|
|
|
|
self.value: int = value
|
2020-03-18 15:15:32 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str) -> Choice:
|
2021-08-09 07:15:41 +00:00
|
|
|
text = text.lower()
|
2021-09-30 11:22:25 +00:00
|
|
|
if text == "random":
|
|
|
|
return cls(random.choice(list(cls.name_lookup)))
|
2022-02-05 14:49:19 +00:00
|
|
|
for option_name, value in cls.options.items():
|
|
|
|
if option_name == text:
|
2020-10-24 04:43:35 +00:00
|
|
|
return cls(value)
|
2020-03-18 15:15:32 +00:00
|
|
|
raise KeyError(
|
2020-10-24 04:43:35 +00:00
|
|
|
f'Could not find option "{text}" for "{cls.__name__}", '
|
|
|
|
f'known options are {", ".join(f"{option}" for option in cls.name_lookup.values())}')
|
2020-03-18 15:15:32 +00:00
|
|
|
|
2021-03-14 07:38:02 +00:00
|
|
|
@classmethod
|
2021-05-09 15:46:26 +00:00
|
|
|
def from_any(cls, data: typing.Any) -> Choice:
|
2021-04-08 17:53:24 +00:00
|
|
|
if type(data) == int and data in cls.options.values():
|
|
|
|
return cls(data)
|
|
|
|
return cls.from_text(str(data))
|
2021-03-14 07:38:02 +00:00
|
|
|
|
2021-08-30 14:31:56 +00:00
|
|
|
def __eq__(self, other):
|
2021-09-04 15:53:09 +00:00
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
return other.value == self.value
|
|
|
|
elif isinstance(other, str):
|
2022-03-31 01:29:45 +00:00
|
|
|
assert other in self.options, f"compared against a str that could never be equal. {self} == {other}"
|
2021-08-30 14:31:56 +00:00
|
|
|
return other == self.current_key
|
2021-08-30 21:07:19 +00:00
|
|
|
elif isinstance(other, int):
|
2022-03-31 01:29:45 +00:00
|
|
|
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
|
2021-08-30 14:31:56 +00:00
|
|
|
return other == self.value
|
2021-09-16 22:17:54 +00:00
|
|
|
elif isinstance(other, bool):
|
2021-08-30 14:31:56 +00:00
|
|
|
return other == bool(self.value)
|
|
|
|
else:
|
|
|
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
|
|
|
|
2021-08-30 21:07:19 +00:00
|
|
|
def __ne__(self, other):
|
2021-09-04 15:53:09 +00:00
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
return other.value != self.value
|
|
|
|
elif isinstance(other, str):
|
2022-04-01 01:42:56 +00:00
|
|
|
assert other in self.options, f"compared against a str that could never be equal. {self} != {other}"
|
2021-08-30 21:07:19 +00:00
|
|
|
return other != self.current_key
|
|
|
|
elif isinstance(other, int):
|
2022-03-31 01:29:45 +00:00
|
|
|
assert other in self.name_lookup, f"compared against am int that could never be equal. {self} != {other}"
|
2021-08-30 21:07:19 +00:00
|
|
|
return other != self.value
|
|
|
|
elif isinstance(other, bool):
|
|
|
|
return other != bool(self.value)
|
2021-10-14 17:42:13 +00:00
|
|
|
elif other is None:
|
|
|
|
return False
|
2021-08-30 21:07:19 +00:00
|
|
|
else:
|
|
|
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
2021-06-08 13:39:34 +00:00
|
|
|
|
2022-01-06 16:03:47 +00:00
|
|
|
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
|
2022-09-17 00:55:33 +00:00
|
|
|
class TextChoice(Choice):
|
|
|
|
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
2023-03-07 07:44:20 +00:00
|
|
|
value: typing.Union[str, int]
|
2022-09-17 00:55:33 +00:00
|
|
|
|
|
|
|
def __init__(self, value: typing.Union[str, int]):
|
|
|
|
assert isinstance(value, str) or isinstance(value, int), \
|
|
|
|
f"{value} is not a valid option for {self.__class__.__name__}"
|
|
|
|
self.value = value
|
|
|
|
|
|
|
|
@property
|
|
|
|
def current_key(self) -> str:
|
|
|
|
if isinstance(self.value, str):
|
|
|
|
return self.value
|
2023-03-07 07:44:20 +00:00
|
|
|
return super().current_key
|
2022-09-17 00:55:33 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str) -> TextChoice:
|
|
|
|
if text.lower() == "random": # chooses a random defined option but won't use any free text options
|
|
|
|
return cls(random.choice(list(cls.name_lookup)))
|
|
|
|
for option_name, value in cls.options.items():
|
|
|
|
if option_name.lower() == text.lower():
|
|
|
|
return cls(value)
|
|
|
|
return cls(text)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_option_name(cls, value: T) -> str:
|
|
|
|
if isinstance(value, str):
|
|
|
|
return value
|
2023-03-07 07:44:20 +00:00
|
|
|
return super().get_option_name(value)
|
2022-09-17 00:55:33 +00:00
|
|
|
|
|
|
|
def __eq__(self, other: typing.Any):
|
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
return other.value == self.value
|
|
|
|
elif isinstance(other, str):
|
|
|
|
if other in self.options:
|
|
|
|
return other == self.current_key
|
|
|
|
return other == self.value
|
|
|
|
elif isinstance(other, int):
|
|
|
|
assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}"
|
|
|
|
return other == self.value
|
|
|
|
elif isinstance(other, bool):
|
|
|
|
return other == bool(self.value)
|
|
|
|
else:
|
|
|
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
|
|
|
|
|
|
|
|
2022-10-12 18:28:32 +00:00
|
|
|
class BossMeta(AssembleOptions):
|
|
|
|
def __new__(mcs, name, bases, attrs):
|
|
|
|
if name != "PlandoBosses":
|
|
|
|
assert "bosses" in attrs, f"Please define valid bosses for {name}"
|
|
|
|
attrs["bosses"] = frozenset((boss.lower() for boss in attrs["bosses"]))
|
|
|
|
assert "locations" in attrs, f"Please define valid locations for {name}"
|
|
|
|
attrs["locations"] = frozenset((location.lower() for location in attrs["locations"]))
|
|
|
|
cls = super().__new__(mcs, name, bases, attrs)
|
|
|
|
assert not cls.duplicate_bosses or "singularity" in cls.options, f"Please define option_singularity for {name}"
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
|
|
|
class PlandoBosses(TextChoice, metaclass=BossMeta):
|
|
|
|
"""Generic boss shuffle option that supports plando. Format expected is
|
|
|
|
'location1-boss1;location2-boss2;shuffle_mode'.
|
|
|
|
If shuffle_mode is not provided in the string, this will be the default shuffle mode. Must override can_place_boss,
|
|
|
|
which passes a plando boss and location. Check if the placement is valid for your game here."""
|
|
|
|
bosses: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]]
|
|
|
|
locations: typing.ClassVar[typing.Union[typing.Set[str], typing.FrozenSet[str]]]
|
|
|
|
|
|
|
|
duplicate_bosses: bool = False
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str):
|
|
|
|
# set all of our text to lower case for name checking
|
|
|
|
text = text.lower()
|
|
|
|
if text == "random":
|
|
|
|
return cls(random.choice(list(cls.options.values())))
|
|
|
|
for option_name, value in cls.options.items():
|
|
|
|
if option_name == text:
|
|
|
|
return cls(value)
|
|
|
|
options = text.split(";")
|
|
|
|
|
|
|
|
# since plando exists in the option verify the plando values given are valid
|
|
|
|
cls.validate_plando_bosses(options)
|
|
|
|
return cls.get_shuffle_mode(options)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_shuffle_mode(cls, option_list: typing.List[str]):
|
|
|
|
# find out what mode of boss shuffle we should use for placing bosses after plando
|
|
|
|
# and add as a string to look nice in the spoiler
|
|
|
|
if "random" in option_list:
|
|
|
|
shuffle = random.choice(list(cls.options))
|
|
|
|
option_list.remove("random")
|
|
|
|
options = ";".join(option_list) + f";{shuffle}"
|
|
|
|
boss_class = cls(options)
|
|
|
|
else:
|
|
|
|
for option in option_list:
|
|
|
|
if option in cls.options:
|
|
|
|
options = ";".join(option_list)
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
if cls.duplicate_bosses and len(option_list) == 1:
|
|
|
|
if cls.valid_boss_name(option_list[0]):
|
|
|
|
# this doesn't exist in this class but it's a forced option for classes where this is called
|
|
|
|
options = option_list[0] + ";singularity"
|
|
|
|
else:
|
|
|
|
options = option_list[0] + f";{cls.name_lookup[cls.default]}"
|
|
|
|
else:
|
|
|
|
options = ";".join(option_list) + f";{cls.name_lookup[cls.default]}"
|
|
|
|
boss_class = cls(options)
|
|
|
|
return boss_class
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
|
|
|
|
used_locations = []
|
|
|
|
used_bosses = []
|
|
|
|
for option in options:
|
|
|
|
# check if a shuffle mode was provided in the incorrect location
|
|
|
|
if option == "random" or option in cls.options:
|
|
|
|
if option != options[-1]:
|
|
|
|
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
|
|
|
|
elif "-" in option:
|
|
|
|
location, boss = option.split("-")
|
|
|
|
if location in used_locations:
|
|
|
|
raise ValueError(f"Duplicate Boss Location {location} not allowed.")
|
|
|
|
if not cls.duplicate_bosses and boss in used_bosses:
|
|
|
|
raise ValueError(f"Duplicate Boss {boss} not allowed.")
|
|
|
|
used_locations.append(location)
|
|
|
|
used_bosses.append(boss)
|
|
|
|
if not cls.valid_boss_name(boss):
|
|
|
|
raise ValueError(f"{boss.title()} is not a valid boss name.")
|
|
|
|
if not cls.valid_location_name(location):
|
|
|
|
raise ValueError(f"{location.title()} is not a valid boss location name.")
|
|
|
|
if not cls.can_place_boss(boss, location):
|
|
|
|
raise ValueError(f"{location.title()} is not a valid location for {boss.title()} to be placed.")
|
|
|
|
else:
|
|
|
|
if cls.duplicate_bosses:
|
|
|
|
if not cls.valid_boss_name(option):
|
|
|
|
raise ValueError(f"{option} is not a valid boss name.")
|
|
|
|
else:
|
|
|
|
raise ValueError(f"{option.title()} is not formatted correctly.")
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def can_place_boss(cls, boss: str, location: str) -> bool:
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def valid_boss_name(cls, value: str) -> bool:
|
|
|
|
return value in cls.bosses
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def valid_location_name(cls, value: str) -> bool:
|
|
|
|
return value in cls.locations
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
2022-10-12 18:28:32 +00:00
|
|
|
if isinstance(self.value, int):
|
|
|
|
return
|
2023-03-07 07:44:20 +00:00
|
|
|
from BaseClasses import PlandoOptions
|
2024-03-03 21:30:51 +00:00
|
|
|
if not (PlandoOptions.bosses & plando_options):
|
2022-10-12 18:28:32 +00:00
|
|
|
# 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]
|
|
|
|
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
|
|
|
|
f"boss shuffle will be used for player {player_name}.")
|
|
|
|
|
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
class Range(NumericOption):
|
2021-06-08 12:15:23 +00:00
|
|
|
range_start = 0
|
|
|
|
range_end = 1
|
2021-06-08 13:39:34 +00:00
|
|
|
|
|
|
|
def __init__(self, value: int):
|
|
|
|
if value < self.range_start:
|
|
|
|
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
|
|
|
elif value > self.range_end:
|
|
|
|
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
|
2021-06-08 19:58:11 +00:00
|
|
|
self.value = value
|
2021-06-08 12:15:23 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str) -> Range:
|
2021-06-08 12:48:00 +00:00
|
|
|
text = text.lower()
|
|
|
|
if text.startswith("random"):
|
2022-06-12 21:33:14 +00:00
|
|
|
return cls.weighted_range(text)
|
2022-04-07 17:42:30 +00:00
|
|
|
elif text == "default" and hasattr(cls, "default"):
|
2022-09-19 20:40:15 +00:00
|
|
|
return cls.from_any(cls.default)
|
2022-05-14 23:50:36 +00:00
|
|
|
elif text == "high":
|
2022-04-07 17:42:30 +00:00
|
|
|
return cls(cls.range_end)
|
2022-05-14 23:50:36 +00:00
|
|
|
elif text == "low":
|
2022-04-07 17:42:30 +00:00
|
|
|
return cls(cls.range_start)
|
2022-05-14 23:50:36 +00:00
|
|
|
elif cls.range_start == 0 \
|
|
|
|
and hasattr(cls, "default") \
|
|
|
|
and cls.default != 0 \
|
|
|
|
and text in ("true", "false"):
|
|
|
|
# these are the conditions where "true" and "false" make sense
|
|
|
|
if text == "true":
|
2022-09-19 20:40:15 +00:00
|
|
|
return cls.from_any(cls.default)
|
2022-05-14 23:50:36 +00:00
|
|
|
else: # "false"
|
|
|
|
return cls(0)
|
2021-06-08 13:39:34 +00:00
|
|
|
return cls(int(text))
|
2021-06-08 12:15:23 +00:00
|
|
|
|
2022-06-12 21:33:14 +00:00
|
|
|
@classmethod
|
|
|
|
def weighted_range(cls, text) -> Range:
|
|
|
|
if text == "random-low":
|
|
|
|
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start))
|
|
|
|
elif text == "random-high":
|
|
|
|
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end))
|
|
|
|
elif text == "random-middle":
|
|
|
|
return cls(cls.triangular(cls.range_start, cls.range_end))
|
|
|
|
elif text.startswith("random-range-"):
|
|
|
|
return cls.custom_range(text)
|
|
|
|
elif text == "random":
|
|
|
|
return cls(random.randint(cls.range_start, cls.range_end))
|
|
|
|
else:
|
|
|
|
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
|
|
|
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
|
|
|
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
|
|
|
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def custom_range(cls, text) -> Range:
|
|
|
|
textsplit = text.split("-")
|
|
|
|
try:
|
|
|
|
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
|
|
|
random_range.sort()
|
|
|
|
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
|
|
|
|
raise Exception(
|
|
|
|
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
|
|
|
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
|
|
|
if text.startswith("random-range-low"):
|
|
|
|
return cls(cls.triangular(random_range[0], random_range[1], random_range[0]))
|
|
|
|
elif text.startswith("random-range-middle"):
|
|
|
|
return cls(cls.triangular(random_range[0], random_range[1]))
|
|
|
|
elif text.startswith("random-range-high"):
|
|
|
|
return cls(cls.triangular(random_range[0], random_range[1], random_range[1]))
|
|
|
|
else:
|
|
|
|
return cls(random.randint(random_range[0], random_range[1]))
|
|
|
|
|
2021-06-08 12:15:23 +00:00
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: typing.Any) -> Range:
|
|
|
|
if type(data) == int:
|
|
|
|
return cls(data)
|
|
|
|
return cls.from_text(str(data))
|
2020-03-18 15:15:32 +00:00
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
@classmethod
|
|
|
|
def get_option_name(cls, value: int) -> str:
|
2021-10-14 17:42:13 +00:00
|
|
|
return str(value)
|
2021-06-08 13:39:34 +00:00
|
|
|
|
2022-04-07 17:42:30 +00:00
|
|
|
def __str__(self) -> str:
|
2021-06-08 19:58:11 +00:00
|
|
|
return str(self.value)
|
|
|
|
|
2022-06-12 21:33:14 +00:00
|
|
|
@staticmethod
|
|
|
|
def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int:
|
|
|
|
return int(round(random.triangular(lower, end, tri), 0))
|
|
|
|
|
|
|
|
|
2023-11-24 23:10:52 +00:00
|
|
|
class NamedRange(Range):
|
2022-06-12 21:33:14 +00:00
|
|
|
special_range_names: typing.Dict[str, int] = {}
|
2022-06-14 01:52:21 +00:00
|
|
|
"""Special Range names have to be all lowercase as matching is done with text.lower()"""
|
2022-06-12 21:33:14 +00:00
|
|
|
|
2023-11-24 23:10:52 +00:00
|
|
|
def __init__(self, value: int) -> None:
|
|
|
|
if value < self.range_start and value not in self.special_range_names.values():
|
|
|
|
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__} " +
|
|
|
|
f"and is also not one of the supported named special values: {self.special_range_names}")
|
|
|
|
elif value > self.range_end and value not in self.special_range_names.values():
|
|
|
|
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
|
|
|
|
f"and is also not one of the supported named special values: {self.special_range_names}")
|
2024-06-13 21:29:39 +00:00
|
|
|
|
|
|
|
# See docstring
|
|
|
|
for key in self.special_range_names:
|
|
|
|
if key != key.lower():
|
|
|
|
raise Exception(f"{self.__class__.__name__} has an invalid special_range_names key: {key}. "
|
|
|
|
f"NamedRange keys must use only lowercase letters, and ideally should be snake_case.")
|
2023-11-24 23:10:52 +00:00
|
|
|
self.value = value
|
|
|
|
|
2022-06-12 21:33:14 +00:00
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str) -> Range:
|
|
|
|
text = text.lower()
|
|
|
|
if text in cls.special_range_names:
|
|
|
|
return cls(cls.special_range_names[text])
|
|
|
|
return super().from_text(text)
|
|
|
|
|
2023-11-24 23:10:52 +00:00
|
|
|
|
2023-04-10 21:54:56 +00:00
|
|
|
class FreezeValidKeys(AssembleOptions):
|
|
|
|
def __new__(mcs, name, bases, attrs):
|
2024-05-20 04:20:01 +00:00
|
|
|
assert not "_valid_keys" in attrs, "'_valid_keys' gets set by FreezeValidKeys, define 'valid_keys' instead."
|
2023-04-10 21:54:56 +00:00
|
|
|
if "valid_keys" in attrs:
|
|
|
|
attrs["_valid_keys"] = frozenset(attrs["valid_keys"])
|
|
|
|
return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs)
|
|
|
|
|
|
|
|
|
|
|
|
class VerifyKeys(metaclass=FreezeValidKeys):
|
|
|
|
valid_keys: typing.Iterable = []
|
|
|
|
_valid_keys: frozenset # gets created by AssembleOptions from valid_keys
|
2022-01-11 21:01:54 +00:00
|
|
|
valid_keys_casefold: bool = False
|
2022-03-21 19:49:54 +00:00
|
|
|
convert_name_groups: bool = False
|
|
|
|
verify_item_name: bool = False
|
|
|
|
verify_location_name: bool = False
|
2022-02-06 15:37:21 +00:00
|
|
|
value: typing.Any
|
2022-01-11 21:01:54 +00:00
|
|
|
|
2024-07-31 15:37:52 +00:00
|
|
|
def verify_keys(self) -> None:
|
|
|
|
if self.valid_keys:
|
|
|
|
data = set(self.value)
|
|
|
|
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
|
|
|
|
extra = dataset - self._valid_keys
|
2022-01-11 21:01:54 +00:00
|
|
|
if extra:
|
2024-07-31 15:37:52 +00:00
|
|
|
raise OptionError(
|
|
|
|
f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
|
|
|
|
f"Allowed keys: {self._valid_keys}."
|
|
|
|
)
|
2022-01-11 21:01:54 +00:00
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
2024-07-31 15:37:52 +00:00
|
|
|
try:
|
|
|
|
self.verify_keys()
|
|
|
|
except OptionError as validation_error:
|
|
|
|
raise OptionError(f"Player {player_name} has invalid option keys:\n{validation_error}")
|
2022-03-21 19:49:54 +00:00
|
|
|
if self.convert_name_groups and self.verify_item_name:
|
|
|
|
new_value = type(self.value)() # empty container of whatever value is
|
|
|
|
for item_name in self.value:
|
|
|
|
new_value |= world.item_name_groups.get(item_name, {item_name})
|
|
|
|
self.value = new_value
|
2023-03-08 21:15:28 +00:00
|
|
|
elif self.convert_name_groups and self.verify_location_name:
|
|
|
|
new_value = type(self.value)()
|
|
|
|
for loc_name in self.value:
|
|
|
|
new_value |= world.location_name_groups.get(loc_name, {loc_name})
|
|
|
|
self.value = new_value
|
2022-02-06 15:37:21 +00:00
|
|
|
if self.verify_item_name:
|
|
|
|
for item_name in self.value:
|
|
|
|
if item_name not in world.item_names:
|
2022-05-09 15:03:16 +00:00
|
|
|
picks = get_fuzzy_results(item_name, world.item_names, limit=1)
|
2022-02-06 15:37:21 +00:00
|
|
|
raise Exception(f"Item {item_name} from option {self} "
|
2022-04-26 09:28:43 +00:00
|
|
|
f"is not a valid item name from {world.game}. "
|
|
|
|
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
2022-02-06 15:37:21 +00:00
|
|
|
elif self.verify_location_name:
|
|
|
|
for location_name in self.value:
|
2022-03-21 19:49:54 +00:00
|
|
|
if location_name not in world.location_names:
|
2022-05-09 15:03:16 +00:00
|
|
|
picks = get_fuzzy_results(location_name, world.location_names, limit=1)
|
2022-02-06 15:37:21 +00:00
|
|
|
raise Exception(f"Location {location_name} from option {self} "
|
2022-04-26 09:28:43 +00:00
|
|
|
f"is not a valid location name from {world.game}. "
|
|
|
|
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
2022-02-06 15:37:21 +00:00
|
|
|
|
2022-01-11 21:01:54 +00:00
|
|
|
|
2023-07-30 23:01:21 +00:00
|
|
|
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
2024-03-12 21:03:57 +00:00
|
|
|
default = {}
|
2021-09-16 22:17:54 +00:00
|
|
|
supports_weighting = False
|
2021-05-09 15:46:26 +00:00
|
|
|
|
|
|
|
def __init__(self, value: typing.Dict[str, typing.Any]):
|
2022-10-23 16:28:09 +00:00
|
|
|
self.value = deepcopy(value)
|
2021-05-09 15:46:26 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
|
|
|
|
if type(data) == dict:
|
|
|
|
return cls(data)
|
|
|
|
else:
|
|
|
|
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
|
|
|
|
2021-08-03 17:09:37 +00:00
|
|
|
def get_option_name(self, value):
|
2021-10-14 17:42:13 +00:00
|
|
|
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
2021-05-09 15:46:26 +00:00
|
|
|
|
2023-07-30 23:01:21 +00:00
|
|
|
def __getitem__(self, item: str) -> typing.Any:
|
|
|
|
return self.value.__getitem__(item)
|
|
|
|
|
|
|
|
def __iter__(self) -> typing.Iterator[str]:
|
|
|
|
return self.value.__iter__()
|
|
|
|
|
|
|
|
def __len__(self) -> int:
|
|
|
|
return self.value.__len__()
|
2021-06-08 13:39:34 +00:00
|
|
|
|
2021-09-30 17:49:36 +00:00
|
|
|
|
2021-10-25 02:13:25 +00:00
|
|
|
class ItemDict(OptionDict):
|
|
|
|
verify_item_name = True
|
|
|
|
|
|
|
|
def __init__(self, value: typing.Dict[str, int]):
|
|
|
|
if any(item_count < 1 for item_count in value.values()):
|
|
|
|
raise Exception("Cannot have non-positive item counts.")
|
|
|
|
super(ItemDict, self).__init__(value)
|
|
|
|
|
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
2023-04-10 21:54:56 +00:00
|
|
|
# Supports duplicate entries and ordering.
|
|
|
|
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
|
|
|
|
# Not a docstring so it doesn't get grabbed by the options system.
|
|
|
|
|
2024-03-12 21:03:57 +00:00
|
|
|
default = ()
|
2021-09-16 22:17:54 +00:00
|
|
|
supports_weighting = False
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
|
2024-03-11 23:30:14 +00:00
|
|
|
def __init__(self, value: typing.Iterable[typing.Any]):
|
2024-03-03 21:30:51 +00:00
|
|
|
self.value = list(deepcopy(value))
|
2021-09-16 22:17:54 +00:00
|
|
|
super(OptionList, self).__init__()
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str):
|
|
|
|
return cls([option.strip() for option in text.split(",")])
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: typing.Any):
|
2024-03-11 23:30:14 +00:00
|
|
|
if is_iterable_except_str(data):
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
return cls(data)
|
|
|
|
return cls.from_text(str(data))
|
|
|
|
|
|
|
|
def get_option_name(self, value):
|
2021-12-02 03:53:52 +00:00
|
|
|
return ", ".join(map(str, value))
|
2021-09-16 22:17:54 +00:00
|
|
|
|
2021-09-30 17:49:36 +00:00
|
|
|
def __contains__(self, item):
|
|
|
|
return item in self.value
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
|
2022-03-27 23:47:47 +00:00
|
|
|
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
2024-03-12 21:03:57 +00:00
|
|
|
default = frozenset()
|
2021-09-16 22:17:54 +00:00
|
|
|
supports_weighting = False
|
|
|
|
|
2022-10-23 16:28:09 +00:00
|
|
|
def __init__(self, value: typing.Iterable[str]):
|
|
|
|
self.value = set(deepcopy(value))
|
2021-09-16 22:17:54 +00:00
|
|
|
super(OptionSet, self).__init__()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_text(cls, text: str):
|
|
|
|
return cls([option.strip() for option in text.split(",")])
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: typing.Any):
|
2024-03-11 23:30:14 +00:00
|
|
|
if is_iterable_except_str(data):
|
2021-09-16 22:17:54 +00:00
|
|
|
return cls(data)
|
|
|
|
return cls.from_text(str(data))
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
def get_option_name(self, value):
|
2022-03-22 14:25:34 +00:00
|
|
|
return ", ".join(sorted(value))
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
|
2021-09-30 17:49:36 +00:00
|
|
|
def __contains__(self, item):
|
|
|
|
return item in self.value
|
|
|
|
|
Ocarina of Time (#64)
* first commit (not including OoT data files yet)
* added some basic options
* rule parser works now at least
* make sure to commit everything this time
* temporary change to BaseClasses for oot
* overworld location graph builds mostly correctly
* adding oot data files
* commenting out world options until later since they only existed to make the RuleParser work
* conversion functions between AP ids and OOT ids
* world graph outputs
* set scrub prices
* itempool generates, entrances connected, way too many options added
* fixed set_rules and set_shop_rules
* temp baseclasses changes
* Reaches the fill step now, old event-based system retained in case the new way breaks
* Song placements and misc fixes everywhere
* temporary changes to make oot work
* changed root exits for AP fill framework
* prevent infinite recursion due to OoT sharing usage of the address field
* age reachability works hopefully, songs are broken again
* working spoiler log generation on beatable-only
* Logic tricks implemented
* need this for logic tricks
* fixed map/compass being placed on Serenade location
* kill unreachable events before filling the world
* add a bunch of utility functions to prepare for rom patching
* move OptionList into generic options
* fixed some silly bugs with OptionList
* properly seed all random behavior (so far)
* ROM generation working
* fix hints trying to get alttp dungeon hint texts
* continue fixing hints
* add oot to network data package
* change item and location IDs to 66000 and 67000 range respectively
* push removed items to precollected items
* fixed various issues with cross-contamination with multiple world generation
* reenable glitched logic (hopefully)
* glitched world files age-check fix
* cleaned up some get_locations calls
* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work
* reenable MQ dungeons
* fix forest mq exception
* made targeting style an option for now, will be cosmetic later
* reminder to move targeting to cosmetics
* some oot option maintenance
* enabled starting time of day
* fixed issue breaking shop slots in multiworld generation
* added "off" option for text shuffle and hints
* shopsanity functionality restored
* change patch file extension
* remove unnecessary utility functions + imports
* update MIT license
* change option to "patch_uncompressed_rom" instead of "compress_rom"
* compliance with new AutoWorld systems
* Kill only internal events, remove non-internal big poe event in code
* re-add the big poe event and handle it correctly
* remove extra method in Range option
* fix typo
* Starting items, starting with consumables option
* do not remove nonexistent item
* move set_shop_rules to after shop items are placed
* some cleanup
* add retries for song placement
* flagged Skull Mask and Mask of Truth as advancement items
* update OoT to use LogicMixin
* Fixed trying to assign starting items from the wrong players
* fixed song retry step
* improved option handling, comments, and starting item replacements
* DefaultOnToggle writes Yes or No to spoiler
* enable compression of output if Compress executable is present
* clean up compression
* check whether (de)compressor exists before running the process
* allow specification of rom path in host.yaml
* check if decompressed file already exists before decompressing again
* fix triforce hunt generation
* rename all the oot state functions with prefix
* OoT: mark triforce pieces as completion goal for triforce hunt
* added overworld and any-dungeon shuffle for dungeon items
* Hide most unshuffled locations and events from the list of locations in spoiler
* build oot option ranges with a generic function instead of defining each separately
* move oot output-type control to host.yaml instead of individual yamls
* implement dungeon song shuffle
* minor improvements to overworld dungeon item shuffle
* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list
* always output patch file to folder, remove option to generate ROM in preparation for removal
* re-add the fix for infinite recursion due to not being light or dark world
* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently
* oot: remove item_names and location_names
* oot: minor fixes
* oot: comment out ROM patching
* oot: only add CollectionState objects on creation if actually needed
* main entrance shuffle method and entrances-based rules
* fix entrances based rules
* disable master quest and big poe count options for client compatibility
* use get_player_name instead of get_player_names
* fix OptionList
* fix oot options for new option system
* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES
* fill AP player name in oot rom with 0 instead of 0xDF
* encode player name with ASCII for fixed-width
* revert oot player name array to 8 bytes per name
* remove Pierre location if fast scarecrow is on
* check player name length
* "free_scarecrow" not "fast_scarecrow"
* OoT locations now properly store the AP ID instead of the oot internal ID
* oot __version__ updates in lockstep with AP version
* pull in unmodified oot cosmetic files
* also grab JSONDump since it's needed apparently
* gather extra needed methods, modify imports
* delete cosmetics log, replace all instances of SettingsList with OOTWorld
* cosmetic options working, except for sound effects (due to ear-safe issues)
* SFX, Music, and Fanfare randomization reenabled
* move OoT data files into the worlds folder
* move Compress and Decompress into oot data folder
* Replace get_all_state with custom method to avoid the cache
* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues
* set data_version to 0
* make Kokiri Sword shuffle off by default
* reenable "Random Choice" for various cosmetic options
* kill Ruto's Letter turnin if open fountain
also fix for shopsanity
* place Buy Goron/Zora Tunic first in shop shuffle
* make ice traps appear as other items instead of breaking generation
* managed to break ice traps on non-major-only
* only handle ice traps if they are on
* fix shopsanity for non-oot games, and write player name instead of player number
* light arrows hint uses player name instead of player number
* Reenable "skip child zelda" option
* fix entrances_based_rules
* fix ganondorf hint if starting with light arrows
* fix dungeonitem shuffle and shopsanity interaction
* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group
* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any
* keep bosses and bombchu bowling chus out of data package
* revert workaround for infinite recursion and fix it properly
* fix shared shop id caches during patching process
* fix shop text box overflows, as much as possible
* add default oot host.yaml option
* add .apz5, .n64, .z64 to gitignore
* Properly document and name all (functioning) OOT options
* clean up some imports
* remove unnecessary files from oot's data
* fix typo in gitignore
* readd the Compress and Decompress utilities, since they are needed for generation
* cleanup of imports and some minor optimizations
* increase shop offset for item IDs to 0xCB
* remove shop item AP ids entirely
* prevent triforce pieces for other players from being received by yourself
* add "excluded" property to Location
* Hint system adapted and reenabled; hints still unseeded
* make hints deterministic with lists instead of sets
* do not allow hints to point to Light Arrows on non-vanilla bridge
* foreign locations hint as their full name in OoT rather than their region
* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated
* consolidate versioning in Utils
* ice traps appear as major items rather than any progression item
* set prescription and claim check as defaults for adult trade item settings
* add oot options to playerSettings
* allow case-insensitive logic tricks in yaml
* fix oot shopsanity option formatting
* Write OoT override info even if local item, enabling local checks to show up immediately in the client
* implement CollectionState.can_live_dmg for oot glitched logic
* filter item names for invalid characters when patching shops
* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world
* set hidden-spoiler items and locations with Shop items to events
* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start
* Fix oot Glitched and No Logic generation
* fix indenting
* Greatly reduce displayed cosmetic options
* Change oot data version to 1
* add apz5 distribution to webhost
* print player name if an ALttP dungeon contains a good item for OoT world
* delete unneeded commented code
* remove OcarinaSongs import to satisfy lint
2021-09-02 12:35:05 +00:00
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
class ItemSet(OptionSet):
|
|
|
|
verify_item_name = True
|
|
|
|
convert_name_groups = True
|
2021-03-20 23:47:17 +00:00
|
|
|
|
2020-03-18 15:15:32 +00:00
|
|
|
|
2024-06-01 11:34:41 +00:00
|
|
|
class PlandoText(typing.NamedTuple):
|
|
|
|
at: str
|
|
|
|
text: typing.List[str]
|
|
|
|
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 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, "
|
|
|
|
f"so text for {player_name} will be ignored.")
|
2024-07-31 15:37:52 +00:00
|
|
|
else:
|
|
|
|
super().verify(world, player_name, plando_options)
|
|
|
|
|
|
|
|
def verify_keys(self) -> None:
|
|
|
|
if self.valid_keys:
|
|
|
|
data = set(text.at for text in self)
|
|
|
|
dataset = set(word.casefold() for word in data) if self.valid_keys_casefold else set(data)
|
|
|
|
extra = dataset - self._valid_keys
|
|
|
|
if extra:
|
|
|
|
raise OptionError(
|
|
|
|
f"Invalid \"at\" placement {', '.join(extra)} in {getattr(self, 'display_name', self)}. "
|
|
|
|
f"Allowed placements: {self._valid_keys}."
|
|
|
|
)
|
2024-06-01 11:34:41 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def from_any(cls, data: PlandoTextsFromAnyType) -> Self:
|
|
|
|
texts: typing.List[PlandoText] = []
|
|
|
|
if isinstance(data, typing.Iterable):
|
|
|
|
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:
|
|
|
|
given_text = text.get("text", [])
|
|
|
|
if isinstance(given_text, str):
|
|
|
|
given_text = [given_text]
|
|
|
|
texts.append(PlandoText(
|
|
|
|
at,
|
|
|
|
given_text,
|
|
|
|
text.get("percentage", 100)
|
|
|
|
))
|
|
|
|
elif isinstance(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)}")
|
|
|
|
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: " ".join(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: 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"]))
|
|
|
|
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):
|
|
|
|
class Direction:
|
|
|
|
entrance = "entrance"
|
|
|
|
exit = "exit"
|
|
|
|
both = "both"
|
|
|
|
|
|
|
|
entrance: str
|
|
|
|
exit: str
|
|
|
|
direction: typing.Literal["entrance", "exit", "both"] # TODO: convert Direction to StrEnum once 3.8 is dropped
|
|
|
|
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."""
|
|
|
|
|
|
|
|
display_name = "Plando Connections"
|
|
|
|
|
|
|
|
default = ()
|
|
|
|
supports_weighting = False
|
|
|
|
|
|
|
|
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__()
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
@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 (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.")
|
|
|
|
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)
|
|
|
|
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 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)
|
|
|
|
if is_iterable_except_str(entrance):
|
|
|
|
entrance = random.choice(sorted(entrance))
|
|
|
|
exit = connection.get("exit", None)
|
|
|
|
if is_iterable_except_str(exit):
|
|
|
|
exit = random.choice(sorted(exit))
|
|
|
|
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 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, "
|
|
|
|
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 == PlandoConnection.Direction.both else
|
|
|
|
"<=" if connection.direction == PlandoConnection.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)
|
|
|
|
|
|
|
|
|
2020-03-18 15:15:32 +00:00
|
|
|
class Accessibility(Choice):
|
2024-07-31 10:13:14 +00:00
|
|
|
"""
|
|
|
|
Set rules for reachability of your items/locations.
|
|
|
|
|
|
|
|
**Full:** ensure everything can be reached and acquired.
|
2024-06-14 22:53:42 +00:00
|
|
|
|
2024-07-31 10:13:14 +00:00
|
|
|
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
2024-06-14 22:53:42 +00:00
|
|
|
"""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Accessibility"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2024-07-31 10:13:14 +00:00
|
|
|
option_full = 0
|
2021-09-16 22:17:54 +00:00
|
|
|
option_minimal = 2
|
|
|
|
alias_none = 2
|
2024-07-31 10:13:14 +00:00
|
|
|
alias_locations = 0
|
|
|
|
alias_items = 0
|
|
|
|
default = 0
|
|
|
|
|
|
|
|
|
|
|
|
class ItemsAccessibility(Accessibility):
|
|
|
|
"""
|
|
|
|
Set rules for reachability of your items/locations.
|
|
|
|
|
|
|
|
**Full:** ensure everything can be reached and acquired.
|
|
|
|
|
|
|
|
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
|
|
|
|
|
|
|
**Items:** ensure all logically relevant items can be acquired. Some items, such as keys, may be self-locking, and
|
|
|
|
some locations may be inaccessible.
|
|
|
|
"""
|
|
|
|
option_items = 1
|
2021-09-16 22:17:54 +00:00
|
|
|
default = 1
|
|
|
|
|
|
|
|
|
2023-11-24 23:10:52 +00:00
|
|
|
class ProgressionBalancing(NamedRange):
|
2024-06-14 22:53:42 +00:00
|
|
|
"""A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
|
|
|
|
2024-06-01 11:07:43 +00:00
|
|
|
A lower setting means more getting stuck. A higher setting means less getting stuck.
|
|
|
|
"""
|
2022-05-11 07:13:21 +00:00
|
|
|
default = 50
|
|
|
|
range_start = 0
|
|
|
|
range_end = 99
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Progression Balancing"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2022-06-12 21:33:14 +00:00
|
|
|
special_range_names = {
|
2022-06-14 01:52:21 +00:00
|
|
|
"disabled": 0,
|
|
|
|
"normal": 50,
|
|
|
|
"extreme": 99,
|
2022-06-12 21:33:14 +00:00
|
|
|
}
|
2021-09-16 22:17:54 +00:00
|
|
|
|
|
|
|
|
2023-10-10 20:30:20 +00:00
|
|
|
class OptionsMetaProperty(type):
|
|
|
|
def __new__(mcs,
|
|
|
|
name: str,
|
|
|
|
bases: typing.Tuple[type, ...],
|
|
|
|
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
|
|
|
|
for attr_type in attrs.values():
|
2024-03-03 21:30:51 +00:00
|
|
|
assert not isinstance(attr_type, AssembleOptions), \
|
2023-10-10 20:30:20 +00:00
|
|
|
f"Options for {name} should be type hinted on the class, not assigned"
|
|
|
|
return super().__new__(mcs, name, bases, attrs)
|
|
|
|
|
|
|
|
@property
|
|
|
|
@functools.lru_cache(maxsize=None)
|
|
|
|
def type_hints(cls) -> typing.Dict[str, typing.Type[Option[typing.Any]]]:
|
|
|
|
"""Returns type hints of the class as a dictionary."""
|
|
|
|
return typing.get_type_hints(cls)
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class CommonOptions(metaclass=OptionsMetaProperty):
|
|
|
|
progression_balancing: ProgressionBalancing
|
|
|
|
accessibility: Accessibility
|
|
|
|
|
|
|
|
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
|
|
|
|
"""
|
|
|
|
Returns a dictionary of [str, Option.value]
|
WebHost: Massive overhaul of options pages (#2614)
* Implement support for option groups. WebHost options pages still need to be updated.
* Remove debug output
* In-progress conversion of player-options to Jinja rendering
* Support "Randomize" button without JS, transpile SCSS to CSS, include map file for later editors
* Un-alphabetize options, add default group name for item/location Option classes, implement more option types
* Re-flow UI generation to avoid printing rows with unsupported or invalid option types, add support for TextChoice options
* Support all remaining option types
* Rendering improvements and CSS fixes for prettiness
* Wrap options in a form, update button styles, fix labels, disable inputs where the default is random, nuke the JS
* Minor CSS tweaks, as recommended by the designer
* Hide JS-required elements in noscript tag. Add JS reactivity to range, named-range, and randomize buttons.
* Fix labels, add JS handling for TextChoice
* Make option groups collapsable
* PEP8 current option_groups progress (#2604)
* Make the python more PEP8 and remove unneeded imports
* remove LocationSet from `Item & Location Options` group
* It's ugly, but YAML generation is working
* Stop generating JSON files for player-options pages
* Do not include ItemDict entries whose values are zero
* Properly format yaml output
* Save options when form is submitted, load options on page load
* Fix options being omitted from the page if a group has an even number of options
* Implement generate-game, escape option descriptions
* Fix "randomize" checkboxes not properly setting YAML options to "random"
* Add a separator between item/location groups and items/locations in their respective lists
* Implement option presets
* Fix docs to detail what actually ended up happening
* implement option groups on webworld to allow dev sorting (#2616)
* Force extremely long item/location/option names with no spaces to text-wrap
* Fix "randomize" button being too wide in single-column display, change page header to include game name
* Update preset select to read "custom" when updating form inputs. Show error message if the user doesn't input a name
* Un-break weighted-options, add option group names to weighted options
* Nuke weighted-options. Set up framework to rebuild it in Jinja.
* Generate styles with scss, remove styles which will be replaced, add placeholders for worlds
* Support Toggle, DefaultOnToggle, and Choice options in weighted-options
* Implement expand/collapse without JS for worlds and option groups
* Properly style set options
* Implement Range and NamedRange. Also, CSS is hard.
* Add support for remaining option types. JS and backend still forthcoming.
* Add JS functionality for collapsing game divs, populating span values on range updates. Add <noscript> tag to warn users with JS disabled.
* Support showing/hiding game divs based on range value for game
* Add support for adding/deleting range rows
* Save settings to localStorage on form submission
* Save deleted options on form submission
* Break weighted-options into a per-game page.
- Break weighted-options into a per-game page
- Add "advanced options" links to supported games page
- Use details/summary tags on supported games, player-options, and weighted-options
- Fix bug preventing previously deleted rows from being removed on page load if JS is enabled
- Move route handling for options pages to options.py
- Remove world handling from weighted-options
* Implement loading previous settings from localStorage on page load if JS is enabled
* Weighted options can now generate YAML files and single-player games
* options pages now respect option visibility settings for simple and complex pages
* Remove `/weighted-settings` redirect, fix weighted-options link on player-options page
* Fix instance of AutoWorld not having access to proper `random`
* Catch instances of frozenset along with set
* Restore word-wrap in tooltips
* Fix word wrap in player-options labels
* Add `dedent` filter to help with formatting tooltips in player-options
* Do not change the ordering of keys when printing yaml files
* Move necessary import out of conditional statement
* Expand only the first option group by default on both options pages
* Respect option visibility when generating yaml template files
* Swap to double quotes
* Replace instances of `/weighted-settings` with `/weighted-options`, swap out incomplete links
* Strip newlines and spaces after applying dedent filter
* Fix documentation for option groups
* Update site map
* Update various docs
* Sort OptionSet lists alphabetically
* Minor style tweak
* Fix extremely long text overflowing tooltips
* Convert player-options to use CSS grid instead of tables
* Do not display link to weighted-options page on supported games if the options page is an external link
* Update worlds/AutoWorld.py
Bugfix by @alwaysintreble
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
* Fix NamedRange options not being properly set if a preset it loaded
* Move option-presets route into options.py
* Include preset name in YAML if not "default" and not "custom"
* Removed macros for PlandoBosses and DefaultOnToggle, as they were handled by their parent classes
* Fix not disabling custom inputs when the randomize button is clicked
* Only sort OptionList and OptionSet valid_keys if they are unordered
* Quick style fixes for player-settings to give `select` elements `text-overflow: ellipsis` and increase base size of left-column
* Prevent showing a horizontal scroll bar on player-options if the browser width was beneath a certain threshold
* Fix a bug in weighted-options which prevented inputting a negative value for new range inputs
---------
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2024-05-18 04:11:57 +00:00
|
|
|
|
2023-10-10 20:30:20 +00:00
|
|
|
:param option_names: names of the options to return
|
|
|
|
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
|
|
|
"""
|
2024-08-12 00:13:45 +00:00
|
|
|
assert option_names, "options.as_dict() was used without any option names."
|
2023-10-10 20:30:20 +00:00
|
|
|
option_results = {}
|
|
|
|
for option_name in option_names:
|
|
|
|
if option_name in type(self).type_hints:
|
|
|
|
if casing == "snake":
|
|
|
|
display_name = option_name
|
|
|
|
elif casing == "camel":
|
|
|
|
split_name = [name.title() for name in option_name.split("_")]
|
|
|
|
split_name[0] = split_name[0].lower()
|
|
|
|
display_name = "".join(split_name)
|
|
|
|
elif casing == "pascal":
|
|
|
|
display_name = "".join([name.title() for name in option_name.split("_")])
|
|
|
|
elif casing == "kebab":
|
|
|
|
display_name = option_name.replace("_", "-")
|
|
|
|
else:
|
|
|
|
raise ValueError(f"{casing} is invalid casing for as_dict. "
|
|
|
|
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
|
2023-10-24 20:50:53 +00:00
|
|
|
value = getattr(self, option_name).value
|
|
|
|
if isinstance(value, set):
|
|
|
|
value = sorted(value)
|
|
|
|
option_results[display_name] = value
|
2023-10-10 20:30:20 +00:00
|
|
|
else:
|
|
|
|
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
|
|
|
return option_results
|
2021-09-16 22:17:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
class LocalItems(ItemSet):
|
|
|
|
"""Forces these items to be in their native world."""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Local Items"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2020-03-18 15:15:32 +00:00
|
|
|
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
class NonLocalItems(ItemSet):
|
|
|
|
"""Forces these items to be outside their native world."""
|
2024-06-01 11:07:43 +00:00
|
|
|
display_name = "Non-local Items"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2021-09-16 22:17:54 +00:00
|
|
|
|
|
|
|
|
2021-10-25 02:13:25 +00:00
|
|
|
class StartInventory(ItemDict):
|
2021-09-16 22:17:54 +00:00
|
|
|
"""Start with these items."""
|
|
|
|
verify_item_name = True
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Start Inventory"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2022-02-02 15:29:29 +00:00
|
|
|
|
|
|
|
|
2023-04-10 19:13:33 +00:00
|
|
|
class StartInventoryPool(StartInventory):
|
|
|
|
"""Start with these items and don't place them in the world.
|
2024-06-14 22:53:42 +00:00
|
|
|
|
|
|
|
The game decides what the replacement items will be.
|
|
|
|
"""
|
2023-04-10 19:13:33 +00:00
|
|
|
verify_item_name = True
|
|
|
|
display_name = "Start Inventory from Pool"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2023-04-10 19:13:33 +00:00
|
|
|
|
|
|
|
|
2021-09-16 22:17:54 +00:00
|
|
|
class StartHints(ItemSet):
|
2024-06-14 22:53:42 +00:00
|
|
|
"""Start with these item's locations prefilled into the ``!hint`` command."""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Start Hints"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2021-09-16 22:17:54 +00:00
|
|
|
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
class LocationSet(OptionSet):
|
|
|
|
verify_location_name = True
|
2023-04-04 17:29:20 +00:00
|
|
|
convert_name_groups = True
|
2023-03-07 07:44:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
class StartLocationHints(LocationSet):
|
2024-06-14 22:53:42 +00:00
|
|
|
"""Start with these locations and their item prefilled into the ``!hint`` command."""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Start Location Hints"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2021-10-03 12:40:25 +00:00
|
|
|
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
class ExcludeLocations(LocationSet):
|
2024-06-14 22:53:42 +00:00
|
|
|
"""Prevent these locations from having an important item."""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Excluded Locations"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2021-09-16 22:17:54 +00:00
|
|
|
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
class PriorityLocations(LocationSet):
|
2024-06-14 22:53:42 +00:00
|
|
|
"""Prevent these locations from having an unimportant item."""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Priority Locations"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2022-02-01 15:36:14 +00:00
|
|
|
|
|
|
|
|
2021-11-01 18:37:47 +00:00
|
|
|
class DeathLink(Toggle):
|
|
|
|
"""When you die, everyone dies. Of course the reverse is true too."""
|
2022-02-02 15:29:29 +00:00
|
|
|
display_name = "Death Link"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2021-11-01 18:37:47 +00:00
|
|
|
|
|
|
|
|
2022-02-05 14:49:19 +00:00
|
|
|
class ItemLinks(OptionList):
|
|
|
|
"""Share part of your item pool with other players."""
|
2023-07-22 00:31:23 +00:00
|
|
|
display_name = "Item Links"
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2022-02-05 14:49:19 +00:00
|
|
|
default = []
|
|
|
|
schema = Schema([
|
|
|
|
{
|
|
|
|
"name": And(str, len),
|
|
|
|
"item_pool": [And(str, len)],
|
2022-05-11 23:37:18 +00:00
|
|
|
Optional("exclude"): [And(str, len)],
|
2022-05-15 14:41:11 +00:00
|
|
|
"replacement_item": Or(And(str, len), None),
|
|
|
|
Optional("local_items"): [And(str, len)],
|
2022-12-07 05:37:47 +00:00
|
|
|
Optional("non_local_items"): [And(str, len)],
|
|
|
|
Optional("link_replacement"): Or(None, bool),
|
2022-02-05 14:49:19 +00:00
|
|
|
}
|
|
|
|
])
|
|
|
|
|
2022-05-15 14:41:11 +00:00
|
|
|
@staticmethod
|
2024-06-01 11:34:41 +00:00
|
|
|
def verify_items(items: typing.List[str], item_link: str, pool_name: str, world,
|
|
|
|
allow_item_groups: bool = True) -> typing.Set:
|
2022-05-15 14:41:11 +00:00
|
|
|
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):
|
|
|
|
picks = get_fuzzy_results(item_name, world.item_names, limit=1)
|
|
|
|
picks_group = get_fuzzy_results(item_name, world.item_name_groups.keys(), limit=1)
|
|
|
|
picks_group = f" or '{picks_group[0][0]}' ({picks_group[0][1]}% sure)" if allow_item_groups else ""
|
|
|
|
|
|
|
|
raise Exception(f"Item {item_name} from item link {item_link} "
|
|
|
|
f"is not a valid item from {world.game} for {pool_name}. "
|
|
|
|
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure){picks_group}")
|
|
|
|
if allow_item_groups:
|
|
|
|
pool |= world.item_name_groups.get(item_name, {item_name})
|
|
|
|
else:
|
|
|
|
pool |= {item_name}
|
|
|
|
return pool
|
|
|
|
|
2023-03-07 07:44:20 +00:00
|
|
|
def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None:
|
2022-12-07 05:37:47 +00:00
|
|
|
link: dict
|
2022-09-17 00:55:33 +00:00
|
|
|
super(ItemLinks, self).verify(world, player_name, plando_options)
|
2022-02-23 23:17:24 +00:00
|
|
|
existing_links = set()
|
2022-02-06 15:37:21 +00:00
|
|
|
for link in self.value:
|
2022-02-23 23:17:24 +00:00
|
|
|
if link["name"] in existing_links:
|
|
|
|
raise Exception(f"You cannot have more than one link named {link['name']}.")
|
|
|
|
existing_links.add(link["name"])
|
2022-05-15 14:41:11 +00:00
|
|
|
|
|
|
|
pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world)
|
|
|
|
local_items = set()
|
|
|
|
non_local_items = set()
|
|
|
|
|
2022-05-11 23:37:18 +00:00
|
|
|
if "exclude" in link:
|
2022-05-15 14:41:11 +00:00
|
|
|
pool -= self.verify_items(link["exclude"], link["name"], "exclude", world)
|
|
|
|
if link["replacement_item"]:
|
|
|
|
self.verify_items([link["replacement_item"]], link["name"], "replacement_item", world, False)
|
|
|
|
if "local_items" in link:
|
|
|
|
local_items = self.verify_items(link["local_items"], link["name"], "local_items", world)
|
|
|
|
local_items &= pool
|
|
|
|
if "non_local_items" in link:
|
|
|
|
non_local_items = self.verify_items(link["non_local_items"], link["name"], "non_local_items", world)
|
|
|
|
non_local_items &= pool
|
|
|
|
|
|
|
|
intersection = local_items.intersection(non_local_items)
|
|
|
|
if intersection:
|
2022-12-07 05:37:47 +00:00
|
|
|
raise Exception(f"item_link {link['name']} has {intersection} "
|
|
|
|
f"items in both its local_items and non_local_items pool.")
|
|
|
|
link.setdefault("link_replacement", None)
|
2024-04-18 16:58:18 +00:00
|
|
|
link["item_pool"] = list(pool)
|
2022-05-15 14:41:11 +00:00
|
|
|
|
|
|
|
|
2024-04-14 18:49:43 +00:00
|
|
|
class Removed(FreeText):
|
|
|
|
"""This Option has been Removed."""
|
2024-06-14 22:53:42 +00:00
|
|
|
rich_text_doc = True
|
2024-04-14 18:49:43 +00:00
|
|
|
default = ""
|
|
|
|
visibility = Visibility.none
|
|
|
|
|
|
|
|
def __init__(self, value: str):
|
|
|
|
if value:
|
|
|
|
raise Exception("Option removed, please update your options file.")
|
|
|
|
super().__init__(value)
|
|
|
|
|
|
|
|
|
2023-10-10 20:30:20 +00:00
|
|
|
@dataclass
|
|
|
|
class PerGameCommonOptions(CommonOptions):
|
|
|
|
local_items: LocalItems
|
|
|
|
non_local_items: NonLocalItems
|
|
|
|
start_inventory: StartInventory
|
|
|
|
start_hints: StartHints
|
|
|
|
start_location_hints: StartLocationHints
|
|
|
|
exclude_locations: ExcludeLocations
|
|
|
|
priority_locations: PriorityLocations
|
|
|
|
item_links: ItemLinks
|
2021-09-16 22:17:54 +00:00
|
|
|
|
2023-04-15 23:57:52 +00:00
|
|
|
|
2024-03-03 13:11:44 +00:00
|
|
|
@dataclass
|
|
|
|
class DeathLinkMixin:
|
|
|
|
death_link: DeathLink
|
|
|
|
|
|
|
|
|
2024-05-19 02:40:41 +00:00
|
|
|
class OptionGroup(typing.NamedTuple):
|
|
|
|
"""Define a grouping of options."""
|
|
|
|
name: str
|
|
|
|
"""Name of the group to categorize these options in for display on the WebHost and in generated YAMLS."""
|
|
|
|
options: typing.List[typing.Type[Option[typing.Any]]]
|
|
|
|
"""Options to be in the defined group."""
|
2024-05-24 05:18:21 +00:00
|
|
|
start_collapsed: bool = False
|
|
|
|
"""Whether the group will start collapsed on the WebHost options pages."""
|
2024-05-19 02:40:41 +00:00
|
|
|
|
|
|
|
|
2024-05-23 22:50:40 +00:00
|
|
|
item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints,
|
|
|
|
StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks]
|
|
|
|
"""
|
|
|
|
Options that are always populated in "Item & Location Options" Option Group. Cannot be moved to another group.
|
|
|
|
If desired, a custom "Item & Location Options" Option Group can be defined, but only for adding additional options to
|
|
|
|
it.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
|
|
|
|
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
|
|
|
|
"""Generates and returns a dictionary for the option groups of a specified world."""
|
|
|
|
option_groups = {option: option_group.name
|
|
|
|
for option_group in world.web.option_groups
|
|
|
|
for option in option_group.options}
|
|
|
|
# add a default option group for uncategorized options to get thrown into
|
|
|
|
ordered_groups = ["Game Options"]
|
|
|
|
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
|
|
|
|
grouped_options = {group: {} for group in ordered_groups}
|
|
|
|
for option_name, option in world.options_dataclass.type_hints.items():
|
|
|
|
if visibility_level & option.visibility:
|
|
|
|
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
|
|
|
|
|
|
|
# if the world doesn't have any ungrouped options, this group will be empty so just remove it
|
|
|
|
if not grouped_options["Game Options"]:
|
|
|
|
del grouped_options["Game Options"]
|
|
|
|
|
|
|
|
return grouped_options
|
|
|
|
|
|
|
|
|
|
|
|
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
|
2023-04-15 23:57:52 +00:00
|
|
|
import os
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
from jinja2 import Template
|
|
|
|
|
|
|
|
from worlds import AutoWorldRegister
|
|
|
|
from Utils import local_path, __version__
|
|
|
|
|
|
|
|
full_path: str
|
|
|
|
|
|
|
|
os.makedirs(target_folder, exist_ok=True)
|
|
|
|
|
|
|
|
# clean out old
|
|
|
|
for file in os.listdir(target_folder):
|
|
|
|
full_path = os.path.join(target_folder, file)
|
|
|
|
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
|
|
|
os.unlink(full_path)
|
|
|
|
|
2023-11-24 23:10:52 +00:00
|
|
|
def dictify_range(option: Range):
|
2023-04-15 23:57:52 +00:00
|
|
|
data = {option.default: 50}
|
|
|
|
for sub_option in ["random", "random-low", "random-high"]:
|
|
|
|
if sub_option != option.default:
|
|
|
|
data[sub_option] = 0
|
|
|
|
|
|
|
|
notes = {}
|
|
|
|
for name, number in getattr(option, "special_range_names", {}).items():
|
|
|
|
notes[name] = f"equivalent to {number}"
|
|
|
|
if number in data:
|
|
|
|
data[name] = data[number]
|
|
|
|
del data[number]
|
|
|
|
else:
|
|
|
|
data[name] = 0
|
|
|
|
|
|
|
|
return data, notes
|
|
|
|
|
2024-06-14 22:48:49 +00:00
|
|
|
def yaml_dump_scalar(scalar) -> str:
|
|
|
|
# yaml dump may add end of document marker and newlines.
|
|
|
|
return yaml.dump(scalar).replace("...\n", "").strip()
|
|
|
|
|
2023-04-15 23:57:52 +00:00
|
|
|
for game_name, world in AutoWorldRegister.world_types.items():
|
|
|
|
if not world.hidden or generate_hidden:
|
2024-06-14 22:48:49 +00:00
|
|
|
option_groups = get_option_groups(world)
|
2023-04-15 23:57:52 +00:00
|
|
|
with open(local_path("data", "options.yaml")) as f:
|
|
|
|
file_data = f.read()
|
|
|
|
res = Template(file_data).render(
|
2024-06-14 22:48:49 +00:00
|
|
|
option_groups=option_groups,
|
|
|
|
__version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar,
|
2023-04-15 23:57:52 +00:00
|
|
|
dictify_range=dictify_range,
|
|
|
|
)
|
|
|
|
|
|
|
|
del file_data
|
|
|
|
|
|
|
|
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
|
|
|
|
f.write(res)
|