diff --git a/BaseClasses.py b/BaseClasses.py index 0989999d398..818bdc1f4a1 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1,20 +1,20 @@ from __future__ import annotations -from argparse import Namespace import copy -from enum import unique, IntEnum, IntFlag -import logging -import json import functools +import json +import logging +import random +import secrets +import typing # this can go away when Python 3.8 support is dropped +from argparse import Namespace from collections import OrderedDict, Counter, deque +from enum import unique, IntEnum, IntFlag from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple -import typing # this can go away when Python 3.8 support is dropped -import secrets -import random +import NetUtils import Options import Utils -import NetUtils class Group(TypedDict, total=False): @@ -48,6 +48,7 @@ class MultiWorld(): precollected_items: Dict[int, List[Item]] state: CollectionState + plando_options: PlandoOptions accessibility: Dict[int, Options.Accessibility] early_items: Dict[int, Dict[str, int]] local_early_items: Dict[int, Dict[str, int]] @@ -160,6 +161,7 @@ def set_player_attr(attr, val): self.custom_data = {} self.worlds = {} self.slot_seeds = {} + self.plando_options = PlandoOptions.none def get_all_ids(self) -> Tuple[int, ...]: return self.player_ids + tuple(self.groups) @@ -1558,6 +1560,7 @@ def write_option(option_key: str, option_obj: type(Options.Option)): Utils.__version__, self.multiworld.seed)) outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm) outfile.write('Players: %d\n' % self.multiworld.players) + outfile.write(f'Plando Options: {self.multiworld.plando_options}\n') AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile) for player in range(1, self.multiworld.players + 1): @@ -1674,6 +1677,45 @@ class Tutorial(NamedTuple): authors: List[str] +class PlandoOptions(IntFlag): + none = 0b0000 + items = 0b0001 + connections = 0b0010 + texts = 0b0100 + bosses = 0b1000 + + @classmethod + def from_option_string(cls, option_string: str) -> PlandoOptions: + result = cls(0) + for part in option_string.split(","): + part = part.strip().lower() + if part: + result = cls._handle_part(part, result) + return result + + @classmethod + def from_set(cls, option_set: Set[str]) -> PlandoOptions: + result = cls(0) + for part in option_set: + result = cls._handle_part(part, result) + return result + + @classmethod + def _handle_part(cls, part: str, base: PlandoOptions) -> PlandoOptions: + try: + part = cls[part] + except Exception as e: + raise KeyError(f"{part} is not a recognized name for a plando module. " + f"Known options: {', '.join(flag.name for flag in cls)}") from e + else: + return base | part + + def __str__(self) -> str: + if self.value: + return ", ".join(flag.name for flag in PlandoOptions if self.value & flag.value) + return "None" + + seeddigits = 20 diff --git a/CommonClient.py b/CommonClient.py index aa11661d5bb..e3c325d2105 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -494,7 +494,7 @@ def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Op self._messagebox.open() return self._messagebox - def _handle_connection_loss(self, msg: str) -> None: + def handle_connection_loss(self, msg: str) -> None: """Helper for logging and displaying a loss of connection. Must be called from an except block.""" exc_info = sys.exc_info() logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True}) @@ -580,14 +580,22 @@ def reconnect_hint() -> str: for msg in decode(data): await process_server_cmd(ctx, msg) logger.warning(f"Disconnected from multiworld server{reconnect_hint()}") + except websockets.InvalidMessage: + # probably encrypted + if address.startswith("ws://"): + await server_loop(ctx, "ws" + address[1:]) + else: + ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage" + f"{reconnect_hint()}") except ConnectionRefusedError: - ctx._handle_connection_loss("Connection refused by the server. May not be running Archipelago on that address or port.") + ctx.handle_connection_loss("Connection refused by the server. " + "May not be running Archipelago on that address or port.") except websockets.InvalidURI: - ctx._handle_connection_loss("Failed to connect to the multiworld server (invalid URI)") + ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)") except OSError: - ctx._handle_connection_loss("Failed to connect to the multiworld server") + ctx.handle_connection_loss("Failed to connect to the multiworld server") except Exception: - ctx._handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}") + ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}") finally: await ctx.connection_closed() if ctx.server_address and ctx.username and not ctx.disconnected_intentionally: diff --git a/Generate.py b/Generate.py index 04c081b4ac4..dadabd7ac6e 100644 --- a/Generate.py +++ b/Generate.py @@ -2,14 +2,13 @@ import argparse import logging +import os import random -import urllib.request +import string import urllib.parse -from typing import Set, Dict, Tuple, Callable, Any, Union -import os +import urllib.request from collections import Counter, ChainMap -import string -import enum +from typing import Dict, Tuple, Callable, Any, Union import ModuleUpdate @@ -18,52 +17,17 @@ import Utils from worlds.alttp import Options as LttPOptions from worlds.generic import PlandoConnection -from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, local_path, user_path +from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path from worlds.alttp.EntranceRandomizer import parse_arguments from Main import main as ERmain -from BaseClasses import seeddigits, get_seed +from BaseClasses import seeddigits, get_seed, PlandoOptions import Options from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister import copy -class PlandoSettings(enum.IntFlag): - items = 0b0001 - connections = 0b0010 - texts = 0b0100 - bosses = 0b1000 - - @classmethod - def from_option_string(cls, option_string: str) -> PlandoSettings: - result = cls(0) - for part in option_string.split(","): - part = part.strip().lower() - if part: - result = cls._handle_part(part, result) - return result - - @classmethod - def from_set(cls, option_set: Set[str]) -> PlandoSettings: - result = cls(0) - for part in option_set: - result = cls._handle_part(part, result) - return result - - @classmethod - def _handle_part(cls, part: str, base: PlandoSettings) -> PlandoSettings: - try: - part = cls[part] - except Exception as e: - raise KeyError(f"{part} is not a recognized name for a plando module. " - f"Known options: {', '.join(flag.name for flag in cls)}") from e - else: - return base | part - def __str__(self) -> str: - if self.value: - return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value) - return "Off" def mystery_argparse(): @@ -97,7 +61,7 @@ def resolve_path(path: str, resolver: Callable[[str], str]) -> str: args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path) if not os.path.isabs(args.meta_file_path): args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path) - args.plando: PlandoSettings = PlandoSettings.from_option_string(args.plando) + args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando) return args, options @@ -170,6 +134,7 @@ def main(args=None, callback=ERmain): f"A mix is also permitted.") erargs = parse_arguments(['--multi', str(args.multi)]) erargs.seed = seed + erargs.plando_options = args.plando erargs.glitch_triforce = options["generator"]["glitch_triforce_room"] erargs.spoiler = args.spoiler erargs.race = args.race @@ -226,7 +191,7 @@ def main(args=None, callback=ERmain): elif not erargs.name[player]: # if name was not specified, generate it from filename erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) - + player += 1 except Exception as e: raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e @@ -443,7 +408,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict: return weights -def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings): +def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions): if option_key in game_weights: try: if not option.supports_weighting: @@ -459,7 +424,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random" -def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses): +def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses): if "linked_options" in weights: weights = roll_linked_options(weights) @@ -472,7 +437,7 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings if tuplize_version(version) > version_tuple: raise Exception(f"Settings reports required version of generator is at least {version}, " f"however generator is of version {__version__}") - required_plando_options = PlandoSettings.from_option_string(requirements.get("plando", "")) + required_plando_options = PlandoOptions.from_option_string(requirements.get("plando", "")) if required_plando_options not in plando_options: if required_plando_options: raise Exception(f"Settings reports required plando module {str(required_plando_options)}, " @@ -506,12 +471,12 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings if option_key not in world_type.option_definitions and \ (option_key not in Options.common_options or option_key in game_weights): handle_option(ret, game_weights, option_key, option, plando_options) - if PlandoSettings.items in plando_options: + if PlandoOptions.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "Minecraft" or ret.game == "Ocarina of Time": # bad hardcoded behavior to make this work for now ret.plando_connections = [] - if PlandoSettings.connections in plando_options: + if PlandoOptions.connections in plando_options: options = game_weights.get("plando_connections", []) for placement in options: if roll_percentage(get_choice("percentage", placement, 100)): @@ -626,7 +591,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}") ret.plando_texts = {} - if PlandoSettings.texts in plando_options: + if PlandoOptions.texts in plando_options: tt = TextTable() tt.removeUnwantedText() options = weights.get("plando_texts", []) @@ -638,7 +603,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.plando_texts[at] = str(get_choice_legacy("text", placement)) ret.plando_connections = [] - if PlandoSettings.connections in plando_options: + if PlandoOptions.connections in plando_options: options = weights.get("plando_connections", []) for placement in options: if roll_percentage(get_choice_legacy("percentage", placement, 100)): diff --git a/Main.py b/Main.py index 78da5285453..cbef89a4747 100644 --- a/Main.py +++ b/Main.py @@ -38,6 +38,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No logger = logging.getLogger() world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) + world.plando_options = args.plando_options world.shuffle = args.shuffle.copy() world.logic = args.logic.copy() @@ -291,27 +292,6 @@ def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ checks_in_area[location.player]["Dark World"].append(location.address) checks_in_area[location.player]["Total"] += 1 - oldmancaves = [] - takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] - for index, take_any in enumerate(takeanyregions): - for region in [world.get_region(take_any, player) for player in - world.get_game_players("A Link to the Past") if world.retro_caves[player]]: - item = world.create_item( - region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], - region.player) - player = region.player - location_id = SHOP_ID_START + total_shop_slots + index - - main_entrance = region.get_connecting_entrance(is_main_entrance) - if main_entrance.parent_region.type == RegionType.LightWorld: - checks_in_area[player]["Light World"].append(location_id) - else: - checks_in_area[player]["Dark World"].append(location_id) - checks_in_area[player]["Total"] += 1 - - er_hint_data[player][location_id] = main_entrance.name - oldmancaves.append(((location_id, player), (item.code, player))) - FillDisabledShopSlots(world) def write_multidata(): diff --git a/MultiServer.py b/MultiServer.py index 1bbbed69faa..1c375ee1d54 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -116,7 +116,8 @@ class Context: "location_check_points": int, "server_password": str, "password": str, - "forfeit_mode": str, + "forfeit_mode": str, # TODO remove around 0.4 + "release_mode": str, "remaining_mode": str, "collect_mode": str, "item_cheat": bool, @@ -134,7 +135,6 @@ class Context: item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') all_item_and_group_names: typing.Dict[str, typing.Set[str]] - forced_auto_forfeits: typing.Dict[str, bool] non_hintable_names: typing.Dict[str, typing.Set[str]] def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, @@ -171,7 +171,7 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.location_check_points = location_check_points self.hints_used = collections.defaultdict(int) self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set) - self.forfeit_mode: str = forfeit_mode + self.release_mode: str = forfeit_mode self.remaining_mode: str = remaining_mode self.collect_mode: str = collect_mode self.item_cheat = item_cheat @@ -204,7 +204,6 @@ def __init__(self, host: str, port: int, server_password: str, password: str, lo self.gamespackage = {} self.item_name_groups = {} self.all_item_and_group_names = {} - self.forced_auto_forfeits = collections.defaultdict(lambda: False) self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() @@ -217,7 +216,6 @@ def _load_game_data(self): self.item_name_groups = {world_name: world.item_name_groups for world_name, world in worlds.AutoWorldRegister.world_types.items()} for world_name, world in worlds.AutoWorldRegister.world_types.items(): - self.forced_auto_forfeits[world_name] = world.forced_auto_forfeit self.non_hintable_names[world_name] = world.hint_blacklist def _init_game_data(self): @@ -317,7 +315,7 @@ def notify_client_multiple(self, client: Client, texts: typing.List[str]): if not client.auth: return if client.version >= print_command_compatability_threshold: - async_start(self.send_msgs(client, + async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts])) else: async_start(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts])) @@ -513,9 +511,10 @@ def get_save(self) -> dict: "group_collected": dict(self.group_collected), "stored_data": self.stored_data, "game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points, - "server_password": self.server_password, "password": self.password, "forfeit_mode": - self.forfeit_mode, "remaining_mode": self.remaining_mode, "collect_mode": - self.collect_mode, "item_cheat": self.item_cheat, "compatibility": self.compatibility} + "server_password": self.server_password, "password": self.password, + "forfeit_mode": self.release_mode, "release_mode": self.release_mode, # TODO remove forfeit_mode around 0.4 + "remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode, + "item_cheat": self.item_cheat, "compatibility": self.compatibility} } @@ -546,7 +545,7 @@ def set_save(self, savedata: dict): self.location_check_points = savedata["game_options"]["location_check_points"] self.server_password = savedata["game_options"]["server_password"] self.password = savedata["game_options"]["password"] - self.forfeit_mode = savedata["game_options"]["forfeit_mode"] + self.release_mode = savedata["game_options"]["release_mode"] self.remaining_mode = savedata["game_options"]["remaining_mode"] self.collect_mode = savedata["game_options"]["collect_mode"] self.item_cheat = savedata["game_options"]["item_cheat"] @@ -658,10 +657,8 @@ def on_goal_achieved(self, client: Client): self.notify_all(finished_msg) if "auto" in self.collect_mode: collect_player(self, client.team, client.slot) - if "auto" in self.forfeit_mode: - forfeit_player(self, client.team, client.slot) - elif self.forced_auto_forfeits[self.games[client.slot]]: - forfeit_player(self, client.team, client.slot) + if "auto" in self.release_mode: + release_player(self, client.team, client.slot) self.save() # save goal completion flag def on_new_hint(self, team: int, slot: int): @@ -734,7 +731,8 @@ async def on_client_connected(ctx: Context, client: Client): def get_permissions(ctx) -> typing.Dict[str, Permission]: return { - "forfeit": Permission.from_text(ctx.forfeit_mode), + "forfeit": Permission.from_text(ctx.release_mode), # TODO remove around 0.4 + "release": Permission.from_text(ctx.release_mode), "remaining": Permission.from_text(ctx.remaining_mode), "collect": Permission.from_text(ctx.collect_mode) } @@ -862,7 +860,7 @@ def update_checked_locations(ctx: Context, team: int, slot: int): [{"cmd": "RoomUpdate", "checked_locations": get_checked_checks(ctx, team, slot)}]) -def forfeit_player(ctx: Context, team: int, slot: int): +def release_player(ctx: Context, team: int, slot: int): """register any locations that are in the multidata""" all_locations = set(ctx.locations[slot]) ctx.notify_all("%s (Team #%d) has released all remaining items from their world." % (ctx.player_names[(team, slot)], team + 1)) @@ -1228,23 +1226,19 @@ def _cmd_status(self, tag:str="") -> bool: def _cmd_release(self) -> bool: """Sends remaining items in your world to their recipients.""" - return self._cmd_forfeit() - - def _cmd_forfeit(self) -> bool: - """Surrender and send your remaining items out to their recipients. Use release in the future.""" if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False): - forfeit_player(self.ctx, self.client.team, self.client.slot) + release_player(self.ctx, self.client.team, self.client.slot) return True - if "enabled" in self.ctx.forfeit_mode: - forfeit_player(self.ctx, self.client.team, self.client.slot) + if "enabled" in self.ctx.release_mode: + release_player(self.ctx, self.client.team, self.client.slot) return True - elif "disabled" in self.ctx.forfeit_mode: + elif "disabled" in self.ctx.release_mode: self.output("Sorry, client item releasing has been disabled on this server. " "You can ask the server admin for a /release") return False else: # is auto or goal if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: - forfeit_player(self.ctx, self.client.team, self.client.slot) + release_player(self.ctx, self.client.team, self.client.slot) return True else: self.output( @@ -1872,23 +1866,18 @@ def _cmd_collect(self, player_name: str) -> bool: @mark_raw def _cmd_release(self, player_name: str) -> bool: - """Send out the remaining items from a player to their intended recipients.""" - return self._cmd_forfeit(player_name) - - @mark_raw - def _cmd_forfeit(self, player_name: str) -> bool: """Send out the remaining items from a player to their intended recipients.""" player = self.resolve_player(player_name) if player: team, slot, _ = player - forfeit_player(self.ctx, team, slot) + release_player(self.ctx, team, slot) return True self.output(f"Could not find player {player_name} to release") return False @mark_raw - def _cmd_allow_forfeit(self, player_name: str) -> bool: + def _cmd_allow_release(self, player_name: str) -> bool: """Allow the specified player to use the !release command.""" player = self.resolve_player(player_name) if player: @@ -1901,7 +1890,7 @@ def _cmd_allow_forfeit(self, player_name: str) -> bool: return False @mark_raw - def _cmd_forbid_forfeit(self, player_name: str) -> bool: + def _cmd_forbid_release(self, player_name: str) -> bool: """"Disallow the specified player from using the !release command.""" player = self.resolve_player(player_name) if player: @@ -2061,7 +2050,7 @@ def attrtype(input_text: str): return input_text setattr(self.ctx, option_name, attrtype(option)) self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}") - if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}: + if option_name in {"forfeit_mode", "release_mode", "remaining_mode", "collect_mode"}: # TODO remove forfeit_mode with 0.4 self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}]) elif option_name in {"hint_cost", "location_check_points"}: self.ctx.broadcast_all([{"cmd": "RoomUpdate", option_name: getattr(self.ctx, option_name)}]) @@ -2106,14 +2095,14 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int) parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int) parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true') - parser.add_argument('--forfeit_mode', default=defaults["forfeit_mode"], nargs='?', + parser.add_argument('--release_mode', default=defaults["release_mode"], nargs='?', choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\ - Select !forfeit Accessibility. (default: %(default)s) - auto: Automatic "forfeit" on goal completion - enabled: !forfeit is always available - disabled: !forfeit is never available - goal: !forfeit can be used after goal completion - auto-enabled: !forfeit is available and automatically triggered on goal completion + Select !release Accessibility. (default: %(default)s) + auto: Automatic "release" on goal completion + enabled: !release is always available + disabled: !release is never available + goal: !release can be used after goal completion + auto-enabled: !release is available and automatically triggered on goal completion ''') parser.add_argument('--collect_mode', default=defaults["collect_mode"], nargs='?', choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\ @@ -2135,7 +2124,7 @@ def parse_args() -> argparse.Namespace: help="automatically shut down the server after this many minutes without new location checks. " "0 to keep running. Not yet implemented.") parser.add_argument('--use_embedded_options', action="store_true", - help='retrieve forfeit, remaining and hint options from the multidata file,' + help='retrieve release, remaining and hint options from the multidata file,' ' instead of host.yaml') parser.add_argument('--compatibility', default=defaults["compatibility"], type=int, help=""" @@ -2177,7 +2166,7 @@ async def main(args: argparse.Namespace): Utils.init_logging("Server", loglevel=args.loglevel.lower()) ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, - args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.collect_mode, + args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode, args.remaining_mode, args.auto_shutdown, args.compatibility, args.log_network) data_filename = args.multidata diff --git a/NetUtils.py b/NetUtils.py index 513ab074fc6..d7ab7a43a1a 100644 --- a/NetUtils.py +++ b/NetUtils.py @@ -86,7 +86,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any: data = obj._asdict() data["class"] = obj.__class__.__name__ return data - if isinstance(obj, (tuple, list, set)): + if isinstance(obj, (tuple, list, set, frozenset)): return tuple(_scan_for_TypedTuples(o) for o in obj) if isinstance(obj, dict): return {key: _scan_for_TypedTuples(value) for key, value in obj.items()} @@ -109,7 +109,7 @@ def get_any_version(data: dict) -> Version: return Version(int(data["major"]), int(data["minor"]), int(data["build"])) -whitelist = { +allowlist = { "NetworkPlayer": NetworkPlayer, "NetworkItem": NetworkItem, "NetworkSlot": NetworkSlot @@ -125,7 +125,7 @@ def _object_hook(o: typing.Any) -> typing.Any: hook = custom_hooks.get(o.get("class", None), None) if hook: return hook(o) - cls = whitelist.get(o.get("class", None), None) + cls = allowlist.get(o.get("class", None), None) if cls: for key in tuple(o): if key not in cls._fields: diff --git a/Options.py b/Options.py index b386eb8e460..53bf3576309 100644 --- a/Options.py +++ b/Options.py @@ -133,10 +133,10 @@ def from_any(cls, data: typing.Any) -> Option[T]: raise NotImplementedError if typing.TYPE_CHECKING: - from Generate import PlandoSettings + from Generate import PlandoOptions from worlds.AutoWorld import World - def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None: + def verify(self, world: World, player_name: str, plando_options: PlandoOptions) -> None: pass else: def verify(self, *args, **kwargs) -> None: @@ -578,8 +578,8 @@ def valid_location_name(cls, value: str) -> bool: def verify(self, world, player_name: str, plando_options) -> None: if isinstance(self.value, int): return - from Generate import PlandoSettings - if not(PlandoSettings.bosses & plando_options): + from Generate import PlandoOptions + if not(PlandoOptions.bosses & plando_options): import logging # plando is disabled but plando options were given so pull the option and change it to an int option = self.value.split(";")[-1] diff --git a/Utils.py b/Utils.py index 5687465142f..51d42551047 100644 --- a/Utils.py +++ b/Utils.py @@ -260,7 +260,7 @@ def get_default_options() -> OptionsType: "disable_item_cheat": False, "location_check_points": 1, "hint_cost": 10, - "forfeit_mode": "goal", + "release_mode": "goal", "collect_mode": "disabled", "remaining_mode": "goal", "auto_shutdown": 0, diff --git a/WebHost.py b/WebHost.py index ce8443dbd9b..d098f6e7fbd 100644 --- a/WebHost.py +++ b/WebHost.py @@ -29,7 +29,7 @@ def get_app(): register() app = raw_app - if os.path.exists(configpath): + if os.path.exists(configpath) and not app.config["TESTING"]: import yaml app.config.from_file(configpath, yaml.safe_load) logging.info(f"Updated config from {configpath}") diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 5ca44231270..b7f215da024 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -12,7 +12,7 @@ def allowed_file(filename): return filename.endswith(('.txt', ".yaml", ".zip")) -from Generate import roll_settings, PlandoSettings +from Generate import roll_settings, PlandoOptions from Utils import parse_yamls @@ -69,7 +69,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]: def roll_options(options: Dict[str, Union[dict, str]], plando_options: Set[str] = frozenset({"bosses", "items", "connections", "texts"})) -> \ Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]: - plando_options = PlandoSettings.from_set(set(plando_options)) + plando_options = PlandoOptions.from_set(set(plando_options)) results = {} rolled_results = {} for filename, text in options.items(): diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 7e886e72a67..c445b413922 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -12,12 +12,12 @@ import time import websockets -from pony.orm import db_session, commit, select +from pony.orm import commit, db_session, select import Utils -from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor -from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless -from .models import Room, Command, db +from MultiServer import ClientMessageProcessor, Context, ServerCommandProcessor, auto_shutdown, server +from Utils import cache_argsless, get_public_ipv4, get_public_ipv6, restricted_loads +from .models import Command, Room, db class CustomClientMessageProcessor(ClientMessageProcessor): @@ -66,7 +66,6 @@ def __init__(self, static_server_data: dict): def _load_game_data(self): for key, value in self.static_server_data.items(): setattr(self, key, value) - self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits) self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names) def listen_to_db_commands(self): @@ -126,7 +125,6 @@ def get_random_port(): def get_static_server_data() -> dict: import worlds data = { - "forced_auto_forfeits": {}, "non_hintable_names": {}, "gamespackage": worlds.network_data_package["games"], "item_name_groups": {world_name: world.item_name_groups for world_name, world in @@ -134,7 +132,6 @@ def get_static_server_data() -> dict: } for world_name, world in worlds.AutoWorldRegister.world_types.items(): - data["forced_auto_forfeits"][world_name] = world.forced_auto_forfeit data["non_hintable_names"][world_name] = world.hint_blacklist return data diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 6a2c720a5d5..cbacd5153f9 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -12,7 +12,7 @@ from pony.orm import commit, db_session from BaseClasses import seeddigits, get_seed -from Generate import handle_name, PlandoSettings +from Generate import handle_name, PlandoOptions from Main import main as ERmain from Utils import __version__ from WebHostLib import app @@ -33,7 +33,7 @@ def get_meta(options_source: dict) -> dict: server_options = { "hint_cost": int(options_source.get("hint_cost", 10)), - "forfeit_mode": options_source.get("forfeit_mode", "goal"), + "release_mode": options_source.get("release_mode", "goal"), "remaining_mode": options_source.get("remaining_mode", "disabled"), "collect_mode": options_source.get("collect_mode", "disabled"), "item_cheat": bool(int(options_source.get("item_cheat", 1))), @@ -119,7 +119,7 @@ def task(): erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 - erargs.plando_options = PlandoSettings.from_set(meta.setdefault("plando_options", + erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options", {"bosses", "items", "connections", "texts"})) name_counter = Counter() diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index f78ec3926dd..aa5467d6283 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -69,10 +69,6 @@ def tutorial(game, file, lang): @app.route('/tutorial/') def tutorial_landing(): - worlds = {} - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - worlds[game] = world return render_template("tutorialLanding.html") diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index 0bb0c5b8dd9..5d4aaffa9d4 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -205,6 +205,11 @@ const buildOptionsTable = (settings, romOpts = false) => { let presetOption = document.createElement('option'); presetOption.innerText = presetName; presetOption.value = settings[setting].value_names[presetName]; + const words = presetOption.innerText.split("_"); + for (let i = 0; i < words.length; i++) { + words[i] = words[i][0].toUpperCase() + words[i].substring(1); + } + presetOption.innerText = words.join(" "); specialRangeSelect.appendChild(presetOption); }); let customOption = document.createElement('option'); diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index eff42700a7e..0e5b1984455 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -40,20 +40,20 @@

Generate Game{% if race %} (Race Mode){% endif %}

-