From 8b992cbf00dd69d187cbc9bad0046b0b1421bded Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 23 May 2024 17:50:40 -0500 Subject: [PATCH] Webhost: Disallow empty option groups (#3369) * move item_and_loc_options out of the meta class and into the Options module * don't allow empty world specified option groups * reuse option_group generation code instead of rewriting it * delete the default group if it's empty * indent --- Options.py | 44 +++++++++++++++++++++++++++++++------------ WebHostLib/options.py | 14 ++------------ worlds/AutoWorld.py | 8 ++------ 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/Options.py b/Options.py index 39fd5676..e11e078a 100644 --- a/Options.py +++ b/Options.py @@ -1132,7 +1132,37 @@ class OptionGroup(typing.NamedTuple): """Options to be in the defined group.""" -def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): +item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, + StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] +""" +Options that are always populated in "Item & Location Options" Option Group. Cannot be moved to another group. +If desired, a custom "Item & Location Options" Option Group can be defined, but only for adding additional options to +it. +""" + + +def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[ + str, typing.Dict[str, typing.Type[Option[typing.Any]]]]: + """Generates and returns a dictionary for the option groups of a specified world.""" + option_groups = {option: option_group.name + for option_group in world.web.option_groups + for option in option_group.options} + # add a default option group for uncategorized options to get thrown into + ordered_groups = ["Game Options"] + [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] + grouped_options = {group: {} for group in ordered_groups} + for option_name, option in world.options_dataclass.type_hints.items(): + if visibility_level & option.visibility: + grouped_options[option_groups.get(option, "Game Options")][option_name] = option + + # if the world doesn't have any ungrouped options, this group will be empty so just remove it + if not grouped_options["Game Options"]: + del grouped_options["Game Options"] + + return grouped_options + + +def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None: import os import yaml @@ -1170,17 +1200,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge for game_name, world in AutoWorldRegister.world_types.items(): if not world.hidden or generate_hidden: - - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - ordered_groups = ["Game Options"] - [ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - if option.visibility >= Visibility.template: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option - + grouped_options = get_option_groups(world) with open(local_path("data", "options.yaml")) as f: file_data = f.read() res = Template(file_data).render( diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 94f173df..bc63ec93 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -27,26 +27,16 @@ def get_world_theme(game_name: str) -> str: def render_options_page(template: str, world_name: str, is_complex: bool = False) -> Union[Response, str]: - visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui world = AutoWorldRegister.world_types[world_name] if world.hidden or world.web.options_page is False: return redirect("games") - - option_groups = {option: option_group.name - for option_group in world.web.option_groups - for option in option_group.options} - ordered_groups = ["Game Options", *[group.name for group in world.web.option_groups]] - grouped_options = {group: {} for group in ordered_groups} - for option_name, option in world.options_dataclass.type_hints.items(): - # Exclude settings from options pages if their visibility is disabled - if visibility_flag in option.visibility: - grouped_options[option_groups.get(option, "Game Options")][option_name] = option + visibility_flag = Options.Visibility.complex_ui if is_complex else Options.Visibility.simple_ui return render_template( template, world_name=world_name, world=world, - option_groups=grouped_options, + option_groups=Options.get_option_groups(world, visibility_level=visibility_flag), issubclass=issubclass, Options=Options, theme=get_world_theme(world_name), diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 32a84f5d..f8bc525e 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -10,10 +10,7 @@ from dataclasses import make_dataclass from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) -from Options import ( - ExcludeLocations, ItemLinks, LocalItems, NonLocalItems, OptionGroup, PerGameCommonOptions, - PriorityLocations, StartHints, StartInventory, StartInventoryPool, StartLocationHints -) +from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions from BaseClasses import CollectionState if TYPE_CHECKING: @@ -119,12 +116,11 @@ class WebWorldRegister(type): # don't allow an option to appear in multiple groups, allow "Item & Location Options" to appear anywhere by the # dev, putting it at the end if they don't define options in it option_groups: List[OptionGroup] = dct.get("option_groups", []) - item_and_loc_options = [LocalItems, NonLocalItems, StartInventory, StartInventoryPool, StartHints, - StartLocationHints, ExcludeLocations, PriorityLocations, ItemLinks] seen_options = [] item_group_in_list = False for group in option_groups: assert group.name != "Game Options", "Game Options is a pre-determined group and can not be defined." + assert group.options, "A custom defined Option Group must contain at least one Option." if group.name == "Item & Location Options": group.options.extend(item_and_loc_options) item_group_in_list = True