| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2023-07-21 19:31:23 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  | import abc | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  | import functools | 
					
						
							| 
									
										
										
										
											2024-03-03 16:30:51 -05:00
										 |  |  | import logging | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  | import math | 
					
						
							|  |  |  | import numbers | 
					
						
							| 
									
										
										
										
											2021-06-08 14:15:23 +02:00
										 |  |  | import random | 
					
						
							| 
									
										
										
										
											2023-07-21 19:31:23 -05:00
										 |  |  | import typing | 
					
						
							| 
									
										
										
										
											2024-04-14 20:49:43 +02:00
										 |  |  | import enum | 
					
						
							| 
									
										
										
										
											2024-09-17 18:33:03 -05:00
										 |  |  | from collections import defaultdict | 
					
						
							| 
									
										
										
										
											2023-07-21 19:31:23 -05:00
										 |  |  | from copy import deepcopy | 
					
						
							| 
									
										
										
										
											2024-03-03 16:30:51 -05:00
										 |  |  | from dataclasses import dataclass | 
					
						
							| 
									
										
										
										
											2023-07-21 19:31:23 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | from schema import And, Optional, Or, Schema | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  | from typing_extensions import Self | 
					
						
							| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-14 16:43:42 -06:00
										 |  |  | from Utils import get_file_safe_name, get_fuzzy_results, is_iterable_except_str, output_path | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | if typing.TYPE_CHECKING: | 
					
						
							| 
									
										
										
										
											2024-09-17 18:33:03 -05:00
										 |  |  |     from BaseClasses import MultiWorld, PlandoOptions | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |     from worlds.AutoWorld import World | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02:00
										 |  |  |     import pathlib | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-21 20:57:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-02 02:22:50 -05: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 00:11:57 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-14 20:49:43 +02: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 10:42:30 -07:00
										 |  |  | class AssembleOptions(abc.ABCMeta): | 
					
						
							| 
									
										
										
										
											2021-04-14 17:51:11 +02:00
										 |  |  |     def __new__(mcs, name, bases, attrs): | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         options = attrs["options"] = {} | 
					
						
							|  |  |  |         name_lookup = attrs["name_lookup"] = {} | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  |         # merge parent class options | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         for base in bases: | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |             if getattr(base, "options", None): | 
					
						
							| 
									
										
										
										
											2021-06-08 21:58:11 +02:00
										 |  |  |                 options.update(base.options) | 
					
						
							| 
									
										
										
										
											2021-08-09 09:15:41 +02:00
										 |  |  |                 name_lookup.update(base.name_lookup) | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if | 
					
						
							| 
									
										
										
										
											2021-03-21 00:47:17 +01:00
										 |  |  |                        name.startswith("option_")} | 
					
						
							| 
									
										
										
										
											2022-03-23 02:28:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-26 01:12:54 +01: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 02:28:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) | 
					
						
							|  |  |  |         options.update(new_options) | 
					
						
							| 
									
										
										
										
											2021-03-21 00:47:17 +01:00
										 |  |  |         # apply aliases, without name_lookup | 
					
						
							| 
									
										
										
										
											2024-06-14 22:50:26 -04:00
										 |  |  |         aliases = attrs["aliases"] = {name[6:].lower(): option_id for name, option_id in attrs.items() if | 
					
						
							|  |  |  |                                       name.startswith("alias_")} | 
					
						
							| 
									
										
										
										
											2022-06-10 13:24:44 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 14:03:57 -07: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 13:24:44 -07:00
										 |  |  |         assert "random" not in aliases, "Choice option 'random' cannot be manually assigned." | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 00:32:30 +02: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 13:24:44 -07:00
										 |  |  |         options.update(aliases) | 
					
						
							| 
									
										
										
										
											2021-07-19 01:02:23 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05: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 16:30:51 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |                 attrs["verify"] = verify | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 assert verifiers, "class Option is supposed to implement def verify" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-19 01:02:23 +02:00
										 |  |  |         # auto-validate schema on __init__ | 
					
						
							|  |  |  |         if "schema" in attrs.keys(): | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01: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 03:42:56 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01: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-19 01:02:23 +02:00
										 |  |  |                     self.value = self.schema.validate(self.value) | 
					
						
							| 
									
										
										
										
											2021-09-30 19:49:36 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |                 cls.__init__ = meta__init__ | 
					
						
							|  |  |  |                 return cls | 
					
						
							| 
									
										
										
										
											2021-09-30 19:49:36 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-14 17:51:11 +02:00
										 |  |  |         return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-01 03:42:56 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-27 16:47:47 -07:00
										 |  |  | T = typing.TypeVar('T') | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-27 16:47:47 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | class Option(typing.Generic[T], metaclass=AssembleOptions): | 
					
						
							|  |  |  |     value: T | 
					
						
							| 
									
										
										
										
											2024-03-12 14:03:57 -07:00
										 |  |  |     default: typing.ClassVar[typing.Any]  # something that __init__ will be able to convert to the correct type | 
					
						
							| 
									
										
										
										
											2024-04-14 20:49:43 +02:00
										 |  |  |     visibility = Visibility.all | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     # convert option_name_long into Name Long as display_name, otherwise name_long is the result. | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  |     # Handled in get_option_name() | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     auto_display_name = False | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     # can be weighted between selections | 
					
						
							|  |  |  |     supports_weighting = True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07: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-04-01 00:47:50 +02:00
										 |  |  |     # filled by AssembleOptions: | 
					
						
							| 
									
										
										
										
											2024-03-12 14:03:57 -07: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-14 22:50:26 -04:00
										 |  |  |     aliases: typing.ClassVar[typing.Dict[str, int]] | 
					
						
							| 
									
										
										
										
											2022-04-01 00:47:50 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  |     def __repr__(self) -> str: | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |         return f"{self.__class__.__name__}({self.current_option_name})" | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |     def __hash__(self) -> int: | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         return hash(self.value) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-09 09:15:41 +02:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def current_key(self) -> str: | 
					
						
							|  |  |  |         return self.name_lookup[self.value] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def current_option_name(self) -> str: | 
					
						
							|  |  |  |         """For display purposes. Worlds should be using current_key.""" | 
					
						
							| 
									
										
										
										
											2021-08-03 19:09:37 +02:00
										 |  |  |         return self.get_option_name(self.value) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-31 22:14:18 +02:00
										 |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2022-04-01 00:47:50 +02:00
										 |  |  |     def get_option_name(cls, value: T) -> str: | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |         if cls.auto_display_name: | 
					
						
							| 
									
										
										
										
											2021-08-31 22:14:18 +02:00
										 |  |  |             return cls.name_lookup[value].replace("_", " ").title() | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  |         else: | 
					
						
							| 
									
										
										
										
											2021-08-31 22:14:18 +02:00
										 |  |  |             return cls.name_lookup[value] | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-01 00:47:50 +02:00
										 |  |  |     def __int__(self) -> T: | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         return self.value | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  |     def __bool__(self) -> bool: | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         return bool(self.value) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2023-01-31 21:26:09 +01:00
										 |  |  |     @abc.abstractmethod | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |     def from_any(cls, data: typing.Any) -> Option[T]: | 
					
						
							| 
									
										
										
										
											2023-01-31 21:26:09 +01:00
										 |  |  |         ... | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |     if typing.TYPE_CHECKING: | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |         def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None: | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |             pass | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         def verify(self, *args, **kwargs) -> None: | 
					
						
							|  |  |  |             pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | class FreeText(Option[str]): | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |     """Text option that allows users to enter strings.
 | 
					
						
							|  |  |  |     Needs to be validated by the world or option definition."""
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 14:03:57 -07:00
										 |  |  |     default = "" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05: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 01:44:20 -06:00
										 |  |  |     def get_option_name(cls, value: str) -> str: | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |         return value | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-12 14:40:16 -05: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 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-31 21:26:09 +01:00
										 |  |  | class NumericOption(Option[int], numbers.Integral, abc.ABC): | 
					
						
							| 
									
										
										
										
											2022-09-28 14:54:10 -07:00
										 |  |  |     default = 0 | 
					
						
							| 
									
										
										
										
											2024-03-03 16:30:51 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07: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 15:30:20 -05: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 10:42:30 -07: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 06:43:35 +02:00
										 |  |  |     option_false = 0 | 
					
						
							|  |  |  |     option_true = 1 | 
					
						
							| 
									
										
										
										
											2021-04-03 14:47:49 +02:00
										 |  |  |     default = 0 | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, value: int): | 
					
						
							| 
									
										
										
										
											2024-04-20 21:37:28 -04:00
										 |  |  |         # if user puts in an invalid value, make it valid | 
					
						
							|  |  |  |         value = int(bool(value)) | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |         self.value = value | 
					
						
							| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_text(cls, text: str) -> Toggle: | 
					
						
							| 
									
										
										
										
											2022-03-31 22:09:54 +02: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 06:43:35 +02:00
										 |  |  |             return cls(0) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             return cls(1) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_any(cls, data: typing.Any): | 
					
						
							|  |  |  |         if type(data) == str: | 
					
						
							|  |  |  |             return cls.from_text(data) | 
					
						
							|  |  |  |         else: | 
					
						
							| 
									
										
										
										
											2022-08-27 09:21:47 +02:00
										 |  |  |             return cls(int(data)) | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-31 22:52:14 +02:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def get_option_name(cls, value): | 
					
						
							| 
									
										
										
										
											2021-08-03 19:09:37 +02:00
										 |  |  |         return ["No", "Yes"][int(value)] | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-06 06:18:54 +01:00
										 |  |  |     __hash__ = Option.__hash__  # see https://docs.python.org/3/reference/datamodel.html#object.__hash__ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-19 01:00:21 +02:00
										 |  |  | class DefaultOnToggle(Toggle): | 
					
						
							|  |  |  |     default = 1 | 
					
						
							| 
									
										
										
										
											2021-04-03 14:47:49 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  | class Choice(NumericOption): | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     auto_display_name = True | 
					
						
							| 
									
										
										
										
											2021-08-03 19:03:41 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |     def __init__(self, value: int): | 
					
						
							|  |  |  |         self.value: int = value | 
					
						
							| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_text(cls, text: str) -> Choice: | 
					
						
							| 
									
										
										
										
											2021-08-09 09:15:41 +02:00
										 |  |  |         text = text.lower() | 
					
						
							| 
									
										
										
										
											2021-09-30 13:22:25 +02:00
										 |  |  |         if text == "random": | 
					
						
							|  |  |  |             return cls(random.choice(list(cls.name_lookup))) | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         for option_name, value in cls.options.items(): | 
					
						
							|  |  |  |             if option_name == text: | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02:00
										 |  |  |                 return cls(value) | 
					
						
							| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  |         raise KeyError( | 
					
						
							| 
									
										
										
										
											2020-10-24 06:43:35 +02: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 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  |     @classmethod | 
					
						
							| 
									
										
										
										
											2021-05-09 17:46:26 +02:00
										 |  |  |     def from_any(cls, data: typing.Any) -> Choice: | 
					
						
							| 
									
										
										
										
											2021-04-08 19:53:24 +02:00
										 |  |  |         if type(data) == int and data in cls.options.values(): | 
					
						
							|  |  |  |             return cls(data) | 
					
						
							|  |  |  |         return cls.from_text(str(data)) | 
					
						
							| 
									
										
										
										
											2021-03-14 08:38:02 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |     def __eq__(self, other): | 
					
						
							| 
									
										
										
										
											2021-09-04 17:53:09 +02:00
										 |  |  |         if isinstance(other, self.__class__): | 
					
						
							|  |  |  |             return other.value == self.value | 
					
						
							|  |  |  |         elif isinstance(other, str): | 
					
						
							| 
									
										
										
										
											2022-03-31 03:29:45 +02:00
										 |  |  |             assert other in self.options, f"compared against a str that could never be equal. {self} == {other}" | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |             return other == self.current_key | 
					
						
							| 
									
										
										
										
											2021-08-30 23:07:19 +02:00
										 |  |  |         elif isinstance(other, int): | 
					
						
							| 
									
										
										
										
											2022-03-31 03:29:45 +02:00
										 |  |  |             assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}" | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |             return other == self.value | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |         elif isinstance(other, bool): | 
					
						
							| 
									
										
										
										
											2021-08-30 16:31:56 +02:00
										 |  |  |             return other == bool(self.value) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-30 23:07:19 +02:00
										 |  |  |     def __ne__(self, other): | 
					
						
							| 
									
										
										
										
											2021-09-04 17:53:09 +02:00
										 |  |  |         if isinstance(other, self.__class__): | 
					
						
							|  |  |  |             return other.value != self.value | 
					
						
							|  |  |  |         elif isinstance(other, str): | 
					
						
							| 
									
										
										
										
											2022-04-01 03:42:56 +02:00
										 |  |  |             assert other in self.options, f"compared against a str that could never be equal. {self} != {other}" | 
					
						
							| 
									
										
										
										
											2021-08-30 23:07:19 +02:00
										 |  |  |             return other != self.current_key | 
					
						
							|  |  |  |         elif isinstance(other, int): | 
					
						
							| 
									
										
										
										
											2022-03-31 03:29:45 +02:00
										 |  |  |             assert other in self.name_lookup, f"compared against am int that could never be equal. {self} != {other}" | 
					
						
							| 
									
										
										
										
											2021-08-30 23:07:19 +02:00
										 |  |  |             return other != self.value | 
					
						
							|  |  |  |         elif isinstance(other, bool): | 
					
						
							|  |  |  |             return other != bool(self.value) | 
					
						
							| 
									
										
										
										
											2021-10-14 19:42:13 +02:00
										 |  |  |         elif other is None: | 
					
						
							|  |  |  |             return False | 
					
						
							| 
									
										
										
										
											2021-08-30 23:07:19 +02:00
										 |  |  |         else: | 
					
						
							|  |  |  |             raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") | 
					
						
							| 
									
										
										
										
											2021-06-08 15:39:34 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-06 17:03:47 +01:00
										 |  |  |     __hash__ = Option.__hash__  # see https://docs.python.org/3/reference/datamodel.html#object.__hash__ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05: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 01:44:20 -06:00
										 |  |  |     value: typing.Union[str, int] | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, value: typing.Union[str, int]): | 
					
						
							|  |  |  |         assert isinstance(value, str) or isinstance(value, int), \ | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |             f"'{value}' is not a valid option for '{self.__class__.__name__}'" | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |         self.value = value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def current_key(self) -> str: | 
					
						
							|  |  |  |         if isinstance(self.value, str): | 
					
						
							|  |  |  |             return self.value | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |         return super().current_key | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05: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 01:44:20 -06:00
										 |  |  |         return super().get_option_name(value) | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05: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 13:28:32 -05: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): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                     raise ValueError(f"'{boss.title()}' is not a valid boss name.") | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05:00
										 |  |  |                 if not cls.valid_location_name(location): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                     raise ValueError(f"'{location.title()}' is not a valid boss location name.") | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05:00
										 |  |  |                 if not cls.can_place_boss(boss, location): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                     raise ValueError(f"'{location.title()}' is not a valid location for {boss.title()} to be placed.") | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 if cls.duplicate_bosses: | 
					
						
							|  |  |  |                     if not cls.valid_boss_name(option): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                         raise ValueError(f"'{option}' is not a valid boss name.") | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05:00
										 |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                     raise ValueError(f"'{option.title()}' is not formatted correctly.") | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @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 01:44:20 -06:00
										 |  |  |     def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05:00
										 |  |  |         if isinstance(self.value, int): | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |         from BaseClasses import PlandoOptions | 
					
						
							| 
									
										
										
										
											2024-03-03 16:30:51 -05:00
										 |  |  |         if not (PlandoOptions.bosses & plando_options): | 
					
						
							| 
									
										
										
										
											2022-10-12 13:28:32 -05: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 10:42:30 -07:00
										 |  |  | class Range(NumericOption): | 
					
						
							| 
									
										
										
										
											2021-06-08 14:15:23 +02:00
										 |  |  |     range_start = 0 | 
					
						
							|  |  |  |     range_end = 1 | 
					
						
							| 
									
										
										
										
											2021-06-08 15:39:34 +02: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 21:58:11 +02:00
										 |  |  |         self.value = value | 
					
						
							| 
									
										
										
										
											2021-06-08 14:15:23 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def from_text(cls, text: str) -> Range: | 
					
						
							| 
									
										
										
										
											2021-06-08 14:48:00 +02:00
										 |  |  |         text = text.lower() | 
					
						
							|  |  |  |         if text.startswith("random"): | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02:00
										 |  |  |             return cls.weighted_range(text) | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |         elif text == "default" and hasattr(cls, "default"): | 
					
						
							| 
									
										
										
										
											2022-09-19 15:40:15 -05:00
										 |  |  |             return cls.from_any(cls.default) | 
					
						
							| 
									
										
										
										
											2022-05-14 16:50:36 -07:00
										 |  |  |         elif text == "high": | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |             return cls(cls.range_end) | 
					
						
							| 
									
										
										
										
											2022-05-14 16:50:36 -07:00
										 |  |  |         elif text == "low": | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |             return cls(cls.range_start) | 
					
						
							| 
									
										
										
										
											2022-05-14 16:50:36 -07: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 15:40:15 -05:00
										 |  |  |                 return cls.from_any(cls.default) | 
					
						
							| 
									
										
										
										
											2022-05-14 16:50:36 -07:00
										 |  |  |             else:  # "false" | 
					
						
							|  |  |  |                 return cls(0) | 
					
						
							| 
									
										
										
										
											2021-06-08 15:39:34 +02:00
										 |  |  |         return cls(int(text)) | 
					
						
							| 
									
										
										
										
											2021-06-08 14:15:23 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02: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 14:15:23 +02: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 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def get_option_name(cls, value: int) -> str: | 
					
						
							| 
									
										
										
										
											2021-10-14 19:42:13 +02:00
										 |  |  |         return str(value) | 
					
						
							| 
									
										
										
										
											2021-06-08 15:39:34 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:42:30 -07:00
										 |  |  |     def __str__(self) -> str: | 
					
						
							| 
									
										
										
										
											2021-06-08 21:58:11 +02:00
										 |  |  |         return str(self.value) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02: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-25 00:10:52 +01:00
										 |  |  | class NamedRange(Range): | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02:00
										 |  |  |     special_range_names: typing.Dict[str, int] = {} | 
					
						
							| 
									
										
										
										
											2022-06-14 03:52:21 +02:00
										 |  |  |     """Special Range names have to be all lowercase as matching is done with text.lower()""" | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-25 00:10:52 +01: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-12-10 14:37:54 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-13 23:29:39 +02: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-25 00:10:52 +01:00
										 |  |  |         self.value = value | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02: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-25 00:10:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-10 23:54:56 +02:00
										 |  |  | class FreezeValidKeys(AssembleOptions): | 
					
						
							|  |  |  |     def __new__(mcs, name, bases, attrs): | 
					
						
							| 
									
										
										
										
											2024-05-20 06:20:01 +02:00
										 |  |  |         assert not "_valid_keys" in attrs, "'_valid_keys' gets set by FreezeValidKeys, define 'valid_keys' instead." | 
					
						
							| 
									
										
										
										
											2023-04-10 23:54:56 +02: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 22:01:54 +01:00
										 |  |  |     valid_keys_casefold: bool = False | 
					
						
							| 
									
										
										
										
											2022-03-21 20:49:54 +01:00
										 |  |  |     convert_name_groups: bool = False | 
					
						
							|  |  |  |     verify_item_name: bool = False | 
					
						
							|  |  |  |     verify_location_name: bool = False | 
					
						
							| 
									
										
										
										
											2022-02-06 16:37:21 +01:00
										 |  |  |     value: typing.Any | 
					
						
							| 
									
										
										
										
											2022-01-11 22:01:54 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 10:37:52 -05: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 22:01:54 +01:00
										 |  |  |             if extra: | 
					
						
							| 
									
										
										
										
											2024-07-31 10:37:52 -05:00
										 |  |  |                 raise OptionError( | 
					
						
							|  |  |  |                     f"Found unexpected key {', '.join(extra)} in {getattr(self, 'display_name', self)}. " | 
					
						
							|  |  |  |                     f"Allowed keys: {self._valid_keys}." | 
					
						
							|  |  |  |                 ) | 
					
						
							| 
									
										
										
										
											2022-01-11 22:01:54 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  |     def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: | 
					
						
							| 
									
										
										
										
											2024-07-31 10:37:52 -05: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 20:49:54 +01: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 15:15:28 -06: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 16:37:21 +01:00
										 |  |  |         if self.verify_item_name: | 
					
						
							|  |  |  |             for item_name in self.value: | 
					
						
							|  |  |  |                 if item_name not in world.item_names: | 
					
						
							| 
									
										
										
										
											2022-05-09 17:03:16 +02:00
										 |  |  |                     picks = get_fuzzy_results(item_name, world.item_names, limit=1) | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                     raise Exception(f"Item '{item_name}' from option '{self}' " | 
					
						
							|  |  |  |                                     f"is not a valid item name from '{world.game}'. " | 
					
						
							| 
									
										
										
										
											2022-04-26 02:28:43 -07:00
										 |  |  |                                     f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") | 
					
						
							| 
									
										
										
										
											2022-02-06 16:37:21 +01:00
										 |  |  |         elif self.verify_location_name: | 
					
						
							|  |  |  |             for location_name in self.value: | 
					
						
							| 
									
										
										
										
											2022-03-21 20:49:54 +01:00
										 |  |  |                 if location_name not in world.location_names: | 
					
						
							| 
									
										
										
										
											2022-05-09 17:03:16 +02:00
										 |  |  |                     picks = get_fuzzy_results(location_name, world.location_names, limit=1) | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                     raise Exception(f"Location '{location_name}' from option '{self}' " | 
					
						
							|  |  |  |                                     f"is not a valid location name from '{world.game}'. " | 
					
						
							| 
									
										
										
										
											2022-04-26 02:28:43 -07:00
										 |  |  |                                     f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)") | 
					
						
							| 
									
										
										
										
											2022-02-06 16:37:21 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 01:42:08 +01:00
										 |  |  |     def __iter__(self) -> typing.Iterator[typing.Any]: | 
					
						
							|  |  |  |         return self.value.__iter__() | 
					
						
							| 
									
										
										
										
											2022-01-11 22:01:54 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-29 01:42:08 +01:00
										 |  |  |      | 
					
						
							| 
									
										
										
										
											2023-07-30 18:01:21 -05:00
										 |  |  | class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]): | 
					
						
							| 
									
										
										
										
											2024-03-12 14:03:57 -07:00
										 |  |  |     default = {} | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     supports_weighting = False | 
					
						
							| 
									
										
										
										
											2021-05-09 17:46:26 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, value: typing.Dict[str, typing.Any]): | 
					
						
							| 
									
										
										
										
											2022-10-23 09:28:09 -07:00
										 |  |  |         self.value = deepcopy(value) | 
					
						
							| 
									
										
										
										
											2021-05-09 17:46:26 +02: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 19:09:37 +02:00
										 |  |  |     def get_option_name(self, value): | 
					
						
							| 
									
										
										
										
											2021-10-14 19:42:13 +02:00
										 |  |  |         return ", ".join(f"{key}: {v}" for key, v in value.items()) | 
					
						
							| 
									
										
										
										
											2021-05-09 17:46:26 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-30 18:01:21 -05: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 15:39:34 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 19:49:36 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-25 04:13:25 +02:00
										 |  |  | class ItemDict(OptionDict): | 
					
						
							|  |  |  |     verify_item_name = True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, value: typing.Dict[str, int]): | 
					
						
							| 
									
										
										
										
											2024-11-30 19:51:26 -08:00
										 |  |  |         if any(item_count is None for item_count in value.values()): | 
					
						
							|  |  |  |             raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .") | 
					
						
							| 
									
										
										
										
											2021-10-25 04:13:25 +02:00
										 |  |  |         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 16:47:47 -07:00
										 |  |  | class OptionList(Option[typing.List[typing.Any]], VerifyKeys): | 
					
						
							| 
									
										
										
										
											2023-04-10 23:54:56 +02: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 14:03:57 -07:00
										 |  |  |     default = () | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02: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 08:35:05 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-11 19:30:14 -04:00
										 |  |  |     def __init__(self, value: typing.Iterable[typing.Any]): | 
					
						
							| 
									
										
										
										
											2024-03-03 16:30:51 -05:00
										 |  |  |         self.value = list(deepcopy(value)) | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02: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 08:35:05 -04: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 19:30:14 -04: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 08:35:05 -04:00
										 |  |  |             return cls(data) | 
					
						
							|  |  |  |         return cls.from_text(str(data)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_option_name(self, value): | 
					
						
							| 
									
										
										
										
											2021-12-01 21:53:52 -06:00
										 |  |  |         return ", ".join(map(str, value)) | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 19:49:36 +02:00
										 |  |  |     def __contains__(self, item): | 
					
						
							|  |  |  |         return item in self.value | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-27 16:47:47 -07:00
										 |  |  | class OptionSet(Option[typing.Set[str]], VerifyKeys): | 
					
						
							| 
									
										
										
										
											2024-03-12 14:03:57 -07:00
										 |  |  |     default = frozenset() | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     supports_weighting = False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-23 09:28:09 -07:00
										 |  |  |     def __init__(self, value: typing.Iterable[str]): | 
					
						
							|  |  |  |         self.value = set(deepcopy(value)) | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02: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 19:30:14 -04:00
										 |  |  |         if is_iterable_except_str(data): | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02: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 08:35:05 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     def get_option_name(self, value): | 
					
						
							| 
									
										
										
										
											2022-03-22 15:25:34 +01: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 08:35:05 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-30 19:49:36 +02: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 08:35:05 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | class ItemSet(OptionSet): | 
					
						
							|  |  |  |     verify_item_name = True | 
					
						
							|  |  |  |     convert_name_groups = True | 
					
						
							| 
									
										
										
										
											2021-03-21 00:47:17 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05: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 10:37:52 -05: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 06:34:41 -05: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: | 
					
						
							| 
									
										
										
										
											2024-09-09 08:56:15 -05:00
										 |  |  |                             if isinstance(at, dict): | 
					
						
							|  |  |  |                                 if at: | 
					
						
							|  |  |  |                                     at = random.choices(list(at.keys()), | 
					
						
							|  |  |  |                                                         weights=list(at.values()), k=1)[0] | 
					
						
							|  |  |  |                                 else: | 
					
						
							|  |  |  |                                     raise OptionError("\"at\" must be a valid string or weighted list of strings!") | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  |                             given_text = text.get("text", []) | 
					
						
							| 
									
										
										
										
											2024-09-09 08:56:15 -05:00
										 |  |  |                             if isinstance(given_text, dict): | 
					
						
							|  |  |  |                                 if not given_text: | 
					
						
							|  |  |  |                                     given_text = [] | 
					
						
							|  |  |  |                                 else: | 
					
						
							|  |  |  |                                     given_text = random.choices(list(given_text.keys()), | 
					
						
							|  |  |  |                                                                 weights=list(given_text.values()), k=1) | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  |                             if isinstance(given_text, str): | 
					
						
							|  |  |  |                                 given_text = [given_text] | 
					
						
							|  |  |  |                             texts.append(PlandoText( | 
					
						
							|  |  |  |                                 at, | 
					
						
							|  |  |  |                                 given_text, | 
					
						
							|  |  |  |                                 text.get("percentage", 100) | 
					
						
							|  |  |  |                             )) | 
					
						
							| 
									
										
										
										
											2024-09-09 08:56:15 -05:00
										 |  |  |                         else: | 
					
						
							|  |  |  |                             raise OptionError("\"at\" must be a valid string or weighted list of strings!") | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  |                 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): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                 raise ValueError(f"'{entrance.title()}' is not a valid entrance.") | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  |             if not cls.validate_exit_name(exit): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                 raise ValueError(f"'{exit.title()}' is not a valid exit.") | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  |             if not cls.can_connect(entrance, exit): | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                 raise ValueError(f"Connection between '{entrance.title()}' and '{exit.title()}' is invalid.") | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     @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 16:15:32 +01:00
										 |  |  | class Accessibility(Choice): | 
					
						
							| 
									
										
										
										
											2024-07-31 05:13:14 -05:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     Set rules for reachability of your items/locations. | 
					
						
							| 
									
										
										
										
											2024-12-10 14:37:54 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 05:13:14 -05:00
										 |  |  |     **Full:** ensure everything can be reached and acquired. | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 05:13:14 -05:00
										 |  |  |     **Minimal:** ensure what is needed to reach your goal can be acquired. | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Accessibility" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2024-07-31 05:13:14 -05:00
										 |  |  |     option_full = 0 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     option_minimal = 2 | 
					
						
							|  |  |  |     alias_none = 2 | 
					
						
							| 
									
										
										
										
											2024-07-31 05:13:14 -05:00
										 |  |  |     alias_locations = 0 | 
					
						
							|  |  |  |     alias_items = 0 | 
					
						
							|  |  |  |     default = 0 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ItemsAccessibility(Accessibility): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Set rules for reachability of your items/locations. | 
					
						
							| 
									
										
										
										
											2024-12-10 14:37:54 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 05:13:14 -05:00
										 |  |  |     **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-17 00:17:54 +02:00
										 |  |  |     default = 1 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-25 00:10:52 +01:00
										 |  |  | class ProgressionBalancing(NamedRange): | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     """A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-01 07:07:43 -04:00
										 |  |  |     A lower setting means more getting stuck. A higher setting means less getting stuck. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2022-05-11 00:13:21 -07:00
										 |  |  |     default = 50 | 
					
						
							|  |  |  |     range_start = 0 | 
					
						
							|  |  |  |     range_end = 99 | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Progression Balancing" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02:00
										 |  |  |     special_range_names = { | 
					
						
							| 
									
										
										
										
											2022-06-14 03:52:21 +02:00
										 |  |  |         "disabled": 0, | 
					
						
							|  |  |  |         "normal": 50, | 
					
						
							|  |  |  |         "extreme": 99, | 
					
						
							| 
									
										
										
										
											2022-06-12 23:33:14 +02:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05: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 16:30:51 -05:00
										 |  |  |             assert not isinstance(attr_type, AssembleOptions), \ | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05: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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-10 14:37:54 -05:00
										 |  |  |     def as_dict(self, | 
					
						
							|  |  |  |                 *option_names: str, | 
					
						
							|  |  |  |                 casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake", | 
					
						
							|  |  |  |                 toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]: | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         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 00:11:57 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |         :param option_names: names of the options to return | 
					
						
							|  |  |  |         :param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab` | 
					
						
							| 
									
										
										
										
											2024-12-10 14:37:54 -05:00
										 |  |  |         :param toggles_as_bools: whether toggle options should be output as bools instead of strings | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2024-08-11 20:13:45 -04:00
										 |  |  |         assert option_names, "options.as_dict() was used without any option names." | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05: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 15:50:53 -05:00
										 |  |  |                 value = getattr(self, option_name).value | 
					
						
							|  |  |  |                 if isinstance(value, set): | 
					
						
							|  |  |  |                     value = sorted(value) | 
					
						
							| 
									
										
										
										
											2024-12-10 14:37:54 -05:00
										 |  |  |                 elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle): | 
					
						
							|  |  |  |                     value = bool(value) | 
					
						
							| 
									
										
										
										
											2023-10-24 15:50:53 -05:00
										 |  |  |                 option_results[display_name] = value | 
					
						
							| 
									
										
										
										
											2023-10-10 15:30:20 -05:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}") | 
					
						
							|  |  |  |         return option_results | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class LocalItems(ItemSet): | 
					
						
							|  |  |  |     """Forces these items to be in their native world.""" | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Local Items" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2020-03-18 16:15:32 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | class NonLocalItems(ItemSet): | 
					
						
							|  |  |  |     """Forces these items to be outside their native world.""" | 
					
						
							| 
									
										
										
										
											2024-06-01 07:07:43 -04:00
										 |  |  |     display_name = "Non-local Items" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-25 04:13:25 +02:00
										 |  |  | class StartInventory(ItemDict): | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  |     """Start with these items.""" | 
					
						
							|  |  |  |     verify_item_name = True | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Start Inventory" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-10 21:13:33 +02:00
										 |  |  | class StartInventoryPool(StartInventory): | 
					
						
							|  |  |  |     """Start with these items and don't place them in the world.
 | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     The game decides what the replacement items will be. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2023-04-10 21:13:33 +02:00
										 |  |  |     verify_item_name = True | 
					
						
							|  |  |  |     display_name = "Start Inventory from Pool" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2023-04-10 21:13:33 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | class StartHints(ItemSet): | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     """Start with these item's locations prefilled into the ``!hint`` command.""" | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Start Hints" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | class LocationSet(OptionSet): | 
					
						
							|  |  |  |     verify_location_name = True | 
					
						
							| 
									
										
										
										
											2023-04-04 12:29:20 -05:00
										 |  |  |     convert_name_groups = True | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class StartLocationHints(LocationSet): | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     """Start with these locations and their item prefilled into the ``!hint`` command.""" | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Start Location Hints" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2021-10-03 14:40:25 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | class ExcludeLocations(LocationSet): | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     """Prevent these locations from having an important item.""" | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Excluded Locations" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2021-09-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-07 01:44:20 -06:00
										 |  |  | class PriorityLocations(LocationSet): | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     """Prevent these locations from having an unimportant item.""" | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Priority Locations" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2022-02-01 16:36:14 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | class DeathLink(Toggle): | 
					
						
							| 
									
										
										
										
											2024-09-17 14:17:41 -07:00
										 |  |  |     """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" | 
					
						
							| 
									
										
										
										
											2022-02-02 16:29:29 +01:00
										 |  |  |     display_name = "Death Link" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2021-11-01 19:37:47 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  | class ItemLinks(OptionList): | 
					
						
							|  |  |  |     """Share part of your item pool with other players.""" | 
					
						
							| 
									
										
										
										
											2023-07-21 19:31:23 -05:00
										 |  |  |     display_name = "Item Links" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |     default = [] | 
					
						
							|  |  |  |     schema = Schema([ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             "name": And(str, len), | 
					
						
							|  |  |  |             "item_pool": [And(str, len)], | 
					
						
							| 
									
										
										
										
											2022-05-11 16:37:18 -07:00
										 |  |  |             Optional("exclude"): [And(str, len)], | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  |             "replacement_item": Or(And(str, len), None), | 
					
						
							|  |  |  |             Optional("local_items"): [And(str, len)], | 
					
						
							| 
									
										
										
										
											2022-12-07 06:37:47 +01:00
										 |  |  |             Optional("non_local_items"): [And(str, len)], | 
					
						
							|  |  |  |             Optional("link_replacement"): Or(None, bool), | 
					
						
							| 
									
										
										
										
											2022-02-05 15:49:19 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |     ]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  |     @staticmethod | 
					
						
							| 
									
										
										
										
											2024-06-01 06:34:41 -05: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 07:41:11 -07: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 "" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-20 02:47:33 +01:00
										 |  |  |                 raise Exception(f"Item '{item_name}' from item link '{item_link}' " | 
					
						
							|  |  |  |                                 f"is not a valid item from '{world.game}' for '{pool_name}'. " | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  |                                 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 01:44:20 -06:00
										 |  |  |     def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: | 
					
						
							| 
									
										
										
										
											2022-12-07 06:37:47 +01:00
										 |  |  |         link: dict | 
					
						
							| 
									
										
										
										
											2022-09-16 19:55:33 -05:00
										 |  |  |         super(ItemLinks, self).verify(world, player_name, plando_options) | 
					
						
							| 
									
										
										
										
											2022-02-23 15:17:24 -08:00
										 |  |  |         existing_links = set() | 
					
						
							| 
									
										
										
										
											2022-02-06 16:37:21 +01:00
										 |  |  |         for link in self.value: | 
					
						
							| 
									
										
										
										
											2022-02-23 15:17:24 -08: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 07:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |             pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world) | 
					
						
							|  |  |  |             local_items = set() | 
					
						
							|  |  |  |             non_local_items = set() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-11 16:37:18 -07:00
										 |  |  |             if "exclude" in link: | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07: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 06:37:47 +01: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 11:58:18 -05:00
										 |  |  |             link["item_pool"] = list(pool) | 
					
						
							| 
									
										
										
										
											2022-05-15 07:41:11 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-14 20:49:43 +02:00
										 |  |  | class Removed(FreeText): | 
					
						
							|  |  |  |     """This Option has been Removed.""" | 
					
						
							| 
									
										
										
										
											2024-06-14 15:53:42 -07:00
										 |  |  |     rich_text_doc = True | 
					
						
							| 
									
										
										
										
											2024-04-14 20:49:43 +02: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 15:30:20 -05: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-17 00:17:54 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-03 07:11:44 -06:00
										 |  |  | @dataclass | 
					
						
							|  |  |  | class DeathLinkMixin: | 
					
						
							|  |  |  |     death_link: DeathLink | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-18 19:40:41 -07: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 00:18:21 -05:00
										 |  |  |     start_collapsed: bool = False | 
					
						
							|  |  |  |     """Whether the group will start collapsed on the WebHost options pages.""" | 
					
						
							| 
									
										
										
										
											2024-05-18 19:40:41 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-23 17:50:40 -05: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.""" | 
					
						
							| 
									
										
										
										
											2024-11-29 16:37:14 -06:00
										 |  |  |     option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ordered_groups = {group.name: group.options for group in world.web.option_groups} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-23 17:50:40 -05:00
										 |  |  |     # add a default option group for uncategorized options to get thrown into | 
					
						
							| 
									
										
										
										
											2024-11-29 16:37:14 -06:00
										 |  |  |     if "Game Options" not in ordered_groups: | 
					
						
							|  |  |  |         grouped_options = set(option for group in ordered_groups.values() for option in group) | 
					
						
							|  |  |  |         ungrouped_options = [option for option in option_to_name if option not in grouped_options] | 
					
						
							|  |  |  |         # only add the game options group if we have ungrouped options | 
					
						
							|  |  |  |         if ungrouped_options: | 
					
						
							|  |  |  |             ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |         group: { | 
					
						
							|  |  |  |             option_to_name[option]: option | 
					
						
							|  |  |  |             for option in group_options | 
					
						
							|  |  |  |             if (visibility_level in option.visibility and option in option_to_name) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         for group, group_options in ordered_groups.items() | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-05-23 17:50:40 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02: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-25 00:10:52 +01:00
										 |  |  |     def dictify_range(option: Range): | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02: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-15 00:48:49 +02: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-16 01:57:52 +02:00
										 |  |  |     for game_name, world in AutoWorldRegister.world_types.items(): | 
					
						
							|  |  |  |         if not world.hidden or generate_hidden: | 
					
						
							| 
									
										
										
										
											2024-06-15 00:48:49 +02:00
										 |  |  |             option_groups = get_option_groups(world) | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02:00
										 |  |  |             with open(local_path("data", "options.yaml")) as f: | 
					
						
							|  |  |  |                 file_data = f.read() | 
					
						
							|  |  |  |             res = Template(file_data).render( | 
					
						
							| 
									
										
										
										
											2024-06-15 00:48:49 +02:00
										 |  |  |                 option_groups=option_groups, | 
					
						
							|  |  |  |                 __version__=__version__, game=game_name, yaml_dump=yaml_dump_scalar, | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02:00
										 |  |  |                 dictify_range=dictify_range, | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             del file_data | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-11-14 16:43:42 -06:00
										 |  |  |             with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f: | 
					
						
							| 
									
										
										
										
											2023-04-16 01:57:52 +02:00
										 |  |  |                 f.write(res) | 
					
						
							| 
									
										
										
										
											2024-09-17 18:33:03 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def dump_player_options(multiworld: MultiWorld) -> None: | 
					
						
							|  |  |  |     from csv import DictWriter | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     game_players = defaultdict(list) | 
					
						
							|  |  |  |     for player, game in multiworld.game.items(): | 
					
						
							|  |  |  |         game_players[game].append(player) | 
					
						
							|  |  |  |     game_players = dict(sorted(game_players.items())) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     output = [] | 
					
						
							|  |  |  |     per_game_option_names = [ | 
					
						
							|  |  |  |         getattr(option, "display_name", option_key) | 
					
						
							|  |  |  |         for option_key, option in PerGameCommonOptions.type_hints.items() | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  |     all_option_names = per_game_option_names.copy() | 
					
						
							|  |  |  |     for game, players in game_players.items(): | 
					
						
							|  |  |  |         game_option_names = per_game_option_names.copy() | 
					
						
							|  |  |  |         for player in players: | 
					
						
							|  |  |  |             world = multiworld.worlds[player] | 
					
						
							|  |  |  |             player_output = { | 
					
						
							|  |  |  |                 "Game": multiworld.game[player], | 
					
						
							|  |  |  |                 "Name": multiworld.get_player_name(player), | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             output.append(player_output) | 
					
						
							|  |  |  |             for option_key, option in world.options_dataclass.type_hints.items(): | 
					
						
							|  |  |  |                 if issubclass(Removed, option): | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 display_name = getattr(option, "display_name", option_key) | 
					
						
							|  |  |  |                 player_output[display_name] = getattr(world.options, option_key).current_option_name | 
					
						
							|  |  |  |                 if display_name not in game_option_names: | 
					
						
							|  |  |  |                     all_option_names.append(display_name) | 
					
						
							|  |  |  |                     game_option_names.append(display_name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file: | 
					
						
							|  |  |  |         fields = ["Game", "Name", *all_option_names] | 
					
						
							|  |  |  |         writer = DictWriter(file, fields) | 
					
						
							|  |  |  |         writer.writeheader() | 
					
						
							|  |  |  |         writer.writerows(output) |