import sys from Options import PerGameCommonOptions, FreeText, Toggle, DefaultOnToggle, Choice, TextChoice, Range, NamedRange, DeathLink, \ OptionGroup, StartInventoryPool, Visibility, item_and_loc_options, Option from .hooks.Options import before_options_defined, after_options_defined, before_option_groups_created, after_option_groups_created from .Data import category_table, game_table, option_table from .Helpers import convert_to_long_string, format_to_valid_identifier from .Locations import victory_names from .Items import item_table from .Game import starting_items from dataclasses import make_dataclass from typing import List, Any, Type import logging class FillerTrapPercent(Range): """How many fillers will be replaced with traps. 0 means no additional traps, 100 means all fillers are traps.""" range_end = 100 class GenerateRegionDiagram(Toggle): """Generate a region diagram.""" visibility = Visibility.none # Hidden option class ManualDeathLink(DeathLink): """When you die, everyone who enabled death link dies. Of course, the reverse is true too.""" def createChoiceOptions(values: dict, aliases: dict) -> dict: values = {'option_' + i: v for i, v in values.items()} aliases = {'alias_' + i: v for i, v in aliases.items()} return {**values, **aliases} def convertOptionVisibility(input) -> Visibility: visibility = Visibility.all if isinstance(input, list): visibility = Visibility.none for type in input: visibility |= Visibility[type.lower()] elif isinstance(input,str): if input.startswith('0b'): visibility = Visibility(int(input, base=0)) else: visibility = Visibility[input.lower()] elif isinstance(input, int): visibility = Visibility(input) return visibility def getOriginalOptionArguments(option: Type[Option[Any]]) -> dict: args = {} args['default'] = option.default if hasattr(option, 'display_name'): args['display'] = option.display_name args['rich_text_doc'] = option.rich_text_doc args['default'] = option.default args['visibility'] = option.visibility return args manual_option_groups: dict[str, List[Type[Option[Any]]]] = {} def addOptionToGroup(option_name: str, group: str): if group not in manual_option_groups.keys(): manual_option_groups[group] = [] if manual_options.get(option_name) and manual_options[option_name] not in manual_option_groups[group]: manual_option_groups[group].append(manual_options[option_name]) ###################### # Manual's default options ###################### manual_options: dict[str, Type[Option[Any]]] = before_options_defined({}) manual_options["start_inventory_from_pool"] = StartInventoryPool manual_options["generate_region_diagram"] = GenerateRegionDiagram if len(victory_names) > 1: if manual_options.get('goal'): logging.warning("Existing Goal option found created via Hooks, it will be overwritten by Manual's generated Goal option.\nIf you want to support old yaml you will need to add alias in after_options_defined") goal: dict[str, Any] = {'option_' + v: i for i, v in enumerate(victory_names)} goal['__module__'] = __name__ manual_options['goal'] = type('goal', (Choice,), dict(goal)) manual_options['goal'].__doc__ = "Choose your victory condition." if any(item.get('trap') for item in item_table): manual_options["filler_traps"] = FillerTrapPercent if game_table.get("death_link"): manual_options["death_link"] = ManualDeathLink ###################### # Option.json options ###################### for option_name, option in option_table.get('core', {}).items(): if option_name.startswith('_'): #To allow commenting out options continue option_display_name = option_name option_name = format_to_valid_identifier(option_name) if manual_options.get(option_name): original_option: Type[Option] = manual_options[option_name] original_doc = str(original_option.__doc__) if issubclass(original_option, Toggle): if option.get('default', None) is not None: option_type = DefaultOnToggle if option['default'] else Toggle if original_option.__base__ != option_type: #only recreate if needed args = getOriginalOptionArguments(original_option) args['__module__'] = __name__ manual_options[option_name] = type(option_name, (option_type,), dict(args)) # Type checker doesn't like having a variable as a base for the type # type: ignore logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {option_type}") elif issubclass(original_option, Choice): if option.get("values"): raise Exception(f"You cannot modify the values of the '{option_display_name}' option since they cannot have their value changed by Option.json") if option.get('aliases'): for alias, value in option['aliases'].items(): original_option.aliases[alias] = value original_option.options.update(original_option.aliases) #for an alias to be valid it must also be in options logging.debug(f"Manual: Option.json modified option '{option_display_name}''s aliases") elif issubclass(original_option, Range): if option.get('values'): #let user add named values args = getOriginalOptionArguments(original_option) args['special_range_names'] = {} if issubclass(original_option, NamedRange): args['special_range_names'] = dict(original_option.special_range_names) args['special_range_names']['default'] = option.get('default', args['special_range_names'].get('default', args['default'])) args['range_start'] = original_option.range_start args['range_end'] = original_option.range_end args['special_range_names'] = {**args['special_range_names'], **{l.lower(): v for l, v in option['values'].items()}} args['__module__'] = __name__ manual_options[option_name] = type(option_name, (NamedRange,), dict(args)) logging.debug(f"Manual: Option.json converted option '{option_display_name}' into a {NamedRange}") manual_options[option_name].display_name = option.get('display_name', option_display_name) # type: ignore manual_options[option_name].__doc__ = convert_to_long_string(option.get('description', original_doc)) if option.get('rich_text_doc'): manual_options[option_name].rich_text_doc = option["rich_text_doc"] if option.get('default'): manual_options[option_name].default = option['default'] if option.get('hidden'): manual_options[option_name].visibility = Visibility.none elif option.get('visibility'): manual_options[option_name].visibility = convertOptionVisibility(option['visibility']) else: logging.debug(f"Manual: Option.json just tried to modify the option '{option_display_name}' but it doesn't currently exists") supported_option_types = ["Toggle", "Choice", "Range"] for option_name, option in option_table.get('user', {}).items(): if option_name.startswith('_'): #To allow commenting out options continue option_display_name = option_name option_name = format_to_valid_identifier(option_name) if manual_options.get(option_name): logging.warning(f"Manual: An option with the name '{option_display_name}' cannot be added since it already exists in Manual Core Options. \nTo modify an existing option move it to the 'core' section of Option.json") else: option_type = option.get('type', "").title() if option_type not in supported_option_types: raise Exception(f'Option {option_display_name} in options.json has an invalid type of "{option["type"]}".\nIt must be one of the folowing: {supported_option_types}') args = {'display_name': option.get('display_name', option_display_name)} # Default to Toggle to prevent hypothetical Unbound option_class option_class: Type[Option] = DefaultOnToggle if option.get('default', False) else Toggle if option_type == "Choice": args = {**args, **createChoiceOptions(option.get('values'), option.get('aliases', {}))} option_class = TextChoice if option.get("allow_custom_value", False) else Choice elif option_type == "Range": args['range_start'] = option.get('range_start', 0) args['range_end'] = option.get('range_end', 1) if option.get('values'): args['special_range_names'] = {l.lower(): v for l, v in option['values'].items()} args['special_range_names']['default'] = option.get('default', args['range_start']) option_class = NamedRange if option.get('values') else Range if option.get('default'): args['default'] = option['default'] if option.get('rich_text_doc',None) is not None: args["rich_text_doc"] = option["rich_text_doc"] if option.get('hidden'): args['visibility'] = Visibility.none elif option.get('visibility'): args['visibility'] = convertOptionVisibility(option['visibility']) args['__module__'] = __name__ manual_options[option_name] = type(option_name, (option_class,), args ) # Same as the first ignore above # type: ignore manual_options[option_name].__doc__ = convert_to_long_string(option.get('description', "an Option")) if option.get('group'): addOptionToGroup(option_name, option['group']) ###################### # category and starting_items options ###################### for category in category_table: for option_name in category_table[category].get("yaml_option", []): if option_name[0] == "!": option_name = option_name[1:] option_name = format_to_valid_identifier(option_name) if option_name not in manual_options: manual_options[option_name] = type(option_name, (DefaultOnToggle,), {"default": True, "__module__": __name__}) manual_options[option_name].__doc__ = "Should items/locations linked to this option be enabled?" if starting_items: for starting_items in starting_items: if starting_items.get("yaml_option"): for option_name in starting_items["yaml_option"]: if option_name[0] == "!": option_name = option_name[1:] option_name = format_to_valid_identifier(option_name) if option_name not in manual_options: manual_options[option_name] = type(option_name, (DefaultOnToggle,), {"default": True, "__module__": __name__}) manual_options[option_name].__doc__ = "Should items/locations linked to this option be enabled?" ###################### # OptionGroups Creation ###################### def make_options_group() -> list[OptionGroup]: global manual_option_groups manual_option_groups = before_option_groups_created(manual_option_groups) option_groups: List[OptionGroup] = [] # For some reason, unless they are added manually, the base item and loc option don't get grouped as they should base_item_loc_group = item_and_loc_options if manual_option_groups: if 'Item & Location Options' in manual_option_groups.keys(): base_item_loc_group = manual_option_groups.pop('Item & Location Options') #Put the custom options before the base AP options base_item_loc_group.extend(item_and_loc_options) if 'Game Options' in manual_option_groups.keys(): # Archipelago automatically assign ungrouped options to this group unless its defined so by deleting it here we let AP recreate it later manual_option_groups.pop('Game Options') for group, options in manual_option_groups.items(): option_groups.append(OptionGroup(group, options)) option_groups.append(OptionGroup('Item & Location Options', base_item_loc_group, True)) return after_option_groups_created(option_groups) manual_options_data = make_dataclass('ManualOptionsClass', manual_options.items(), bases=(PerGameCommonOptions,)) after_options_defined(manual_options_data) # Make the options available in this module for import, needed for WebWorld compatibility this = sys.modules[__name__] for name, obj in manual_options.items(): setattr(this, name, obj) del this