import json import logging import os import typing import Options from Utils import local_path from worlds.AutoWorld import AutoWorldRegister handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", "exclude_locations", "priority_locations"} def create(): target_folder = local_path("WebHostLib", "static", "generated") yaml_folder = os.path.join(target_folder, "configs") Options.generate_yaml_templates(yaml_folder) def get_html_doc(option_type: type(Options.Option)) -> str: if not option_type.__doc__: return "Please document me!" return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip() weighted_options = { "baseOptions": { "description": "Generated by https://archipelago.gg/", "name": "", "game": {}, }, "games": {}, } for game_name, world in AutoWorldRegister.world_types.items(): all_options: typing.Dict[str, Options.AssembleOptions] = world.options_dataclass.type_hints # Generate JSON files for player-options pages player_options = { "baseOptions": { "description": f"Generated by https://archipelago.gg/ for {game_name}", "game": game_name, "name": "", }, } game_options = {} visible: typing.Set[str] = set() visible_weighted: typing.Set[str] = set() for option_name, option in all_options.items(): if option.visibility & Options.Visibility.simple_ui: visible.add(option_name) if option.visibility & Options.Visibility.complex_ui: visible_weighted.add(option_name) if option_name in handled_in_js: pass elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle): game_options[option_name] = this_option = { "type": "select", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), "defaultValue": None, "options": [] } for sub_option_id, sub_option_name in option.name_lookup.items(): if sub_option_name != "random": this_option["options"].append({ "name": option.get_option_name(sub_option_id), "value": sub_option_name, }) if sub_option_id == option.default: this_option["defaultValue"] = sub_option_name if not this_option["defaultValue"]: this_option["defaultValue"] = "random" elif issubclass(option, Options.Range): game_options[option_name] = { "type": "range", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), "defaultValue": option.default if hasattr( option, "default") and option.default != "random" else option.range_start, "min": option.range_start, "max": option.range_end, } if issubclass(option, Options.NamedRange): game_options[option_name]["type"] = 'named_range' game_options[option_name]["value_names"] = {} for key, val in option.special_range_names.items(): game_options[option_name]["value_names"][key] = val elif issubclass(option, Options.ItemSet): game_options[option_name] = { "type": "items-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), "defaultValue": list(option.default) } elif issubclass(option, Options.LocationSet): game_options[option_name] = { "type": "locations-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), "defaultValue": list(option.default) } elif issubclass(option, Options.VerifyKeys) and not issubclass(option, Options.OptionDict): if option.valid_keys: game_options[option_name] = { "type": "custom-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), "options": list(option.valid_keys), "defaultValue": list(option.default) if hasattr(option, "default") else [] } else: logging.debug(f"{option} not exported to Web Options.") player_options["presetOptions"] = {} for preset_name, preset in world.web.options_presets.items(): player_options["presetOptions"][preset_name] = {} for option_name, option_value in preset.items(): # Random range type settings are not valid. assert (not str(option_value).startswith("random-")), \ f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. Special random " \ f"values are not supported for presets." # Normal random is supported, but needs to be handled explicitly. if option_value == "random": player_options["presetOptions"][preset_name][option_name] = option_value continue option = world.options_dataclass.type_hints[option_name].from_any(option_value) if isinstance(option, Options.NamedRange) and isinstance(option_value, str): assert option_value in option.special_range_names, \ f"Invalid preset value '{option_value}' for '{option_name}' in '{preset_name}'. " \ f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}." # Still use the true value for the option, not the name. player_options["presetOptions"][preset_name][option_name] = option.value elif isinstance(option, Options.Range): player_options["presetOptions"][preset_name][option_name] = option.value elif isinstance(option_value, str): # For Choice and Toggle options, the value should be the name of the option. This is to prevent # setting a preset for an option with an overridden from_text method that would normally be okay, # but would not be okay for the webhost's current implementation of player options UI. assert option.name_lookup[option.value] == option_value, \ f"Invalid option value '{option_value}' for '{option_name}' in preset '{preset_name}'. " \ f"Values must not be resolved to a different option via option.from_text (or an alias)." player_options["presetOptions"][preset_name][option_name] = option.current_key else: # int and bool values are fine, just resolve them to the current key for webhost. player_options["presetOptions"][preset_name][option_name] = option.current_key os.makedirs(os.path.join(target_folder, 'player-options'), exist_ok=True) filtered_player_options = player_options filtered_player_options["gameOptions"] = { option_name: option_data for option_name, option_data in game_options.items() if option_name in visible } with open(os.path.join(target_folder, 'player-options', game_name + ".json"), "w") as f: json.dump(filtered_player_options, f, indent=2, separators=(',', ': ')) filtered_player_options["gameOptions"] = { option_name: option_data for option_name, option_data in game_options.items() if option_name in visible_weighted } if not world.hidden and world.web.options_page is True: # Add the random option to Choice, TextChoice, and Toggle options for option in filtered_player_options["gameOptions"].values(): if option["type"] == "select": option["options"].append({"name": "Random", "value": "random"}) if not option["defaultValue"]: option["defaultValue"] = "random" weighted_options["baseOptions"]["game"][game_name] = 0 weighted_options["games"][game_name] = { "gameSettings": filtered_player_options["gameOptions"], "gameItems": tuple(world.item_names), "gameItemGroups": [ group for group in world.item_name_groups.keys() if group != "Everything" ], "gameItemDescriptions": world.item_descriptions, "gameLocations": tuple(world.location_names), "gameLocationGroups": [ group for group in world.location_name_groups.keys() if group != "Everywhere" ], "gameLocationDescriptions": world.location_descriptions, } with open(os.path.join(target_folder, 'weighted-options.json'), "w") as f: json.dump(weighted_options, f, indent=2, separators=(',', ': '))