From f1c5c9a14868e3faa3432769a1b02b81f01b697d Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 6 Aug 2022 11:25:37 +0000 Subject: [PATCH 01/19] WebHost: Fix `OptionDict`s that define `valid_keys` from outputting as `[]` on Weighted Settings export. (#874) * WebHost: Fix OptionDicts that define valid_keys from outputting as [] on Weighted Settings export --- WebHostLib/options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 2cab7728da..ccd1b27b3c 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -110,7 +110,7 @@ def create(): if option.default == "random": this_option["defaultValue"] = "random" - elif hasattr(option, "range_start") and hasattr(option, "range_end"): + elif issubclass(option, Options.Range): game_options[option_name] = { "type": "range", "displayName": option.display_name if hasattr(option, "display_name") else option_name, @@ -121,7 +121,7 @@ def create(): "max": option.range_end, } - if hasattr(option, "special_range_names"): + if issubclass(option, Options.SpecialRange): game_options[option_name]["type"] = 'special_range' game_options[option_name]["value_names"] = {} for key, val in option.special_range_names.items(): @@ -141,7 +141,7 @@ def create(): "description": option.__doc__ if option.__doc__ else "Please document me!", } - elif hasattr(option, "valid_keys"): + elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): if option.valid_keys: game_options[option_name] = { "type": "custom-list", From 9167e5363d6775351a276035e0254c68f37f7a7a Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 6 Aug 2022 07:26:02 -0400 Subject: [PATCH 02/19] DKC3: Correct File Extension in Setup Guide (#872) --- worlds/dkc3/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 3cb3db0a30..471248deb8 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -69,7 +69,7 @@ validator page: [YAML Validation page](/mysterycheck) When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch -files. Your patch file should have a `.apsm` extension. +files. Your patch file should have a `.apdkc3` extension. Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the client, and will also create your ROM in the same place as your patch file. From 04eef669f910532ae3e33334096bac5d31f98b64 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 6 Aug 2022 21:36:32 -0500 Subject: [PATCH 03/19] StS: Add a description for the game. (#876) --- worlds/spire/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/spire/__init__.py b/worlds/spire/__init__.py index 594605cd34..4d2917aab9 100644 --- a/worlds/spire/__init__.py +++ b/worlds/spire/__init__.py @@ -22,6 +22,11 @@ class SpireWeb(WebWorld): class SpireWorld(World): + """ + A deck-building roguelike where you must craft a unique deck, encounter bizarre creatures, discover relics of + immense power, and Slay the Spire! + """ + options = spire_options game = "Slay the Spire" topology_present = False From 181cc470795fcd5a3460333bfd3c397467bd7c7d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 4 Aug 2022 14:10:58 +0200 Subject: [PATCH 04/19] Core: cleanup BaseClasses.Location This is just cleanup and has virtually no performance impact. --- BaseClasses.py | 19 +++++++++---------- worlds/alttp/Shops.py | 4 ++-- worlds/alttp/SubClasses.py | 13 +++++++++---- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index b550569e48..02a194eef5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1064,26 +1064,25 @@ class LocationProgressType(IntEnum): class Location: - # If given as integer, then this is the shop's inventory index - shop_slot: Optional[int] = None - shop_slot_disabled: bool = False + game: str = "Generic" + player: int + name: str + address: Optional[int] + parent_region: Optional[Region] event: bool = False locked: bool = False - game: str = "Generic" show_in_spoiler: bool = True - crystal: bool = False progress_type: LocationProgressType = LocationProgressType.DEFAULT always_allow = staticmethod(lambda item, state: False) access_rule = staticmethod(lambda state: True) item_rule = staticmethod(lambda item: True) item: Optional[Item] = None - parent_region: Optional[Region] - def __init__(self, player: int, name: str = '', address: int = None, parent=None): - self.name: str = name - self.address: Optional[int] = address + def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None): + self.player = player + self.name = name + self.address = address self.parent_region = parent - self.player: int = player def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 9a77e7d11a..f6233286f0 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -207,10 +207,10 @@ def ShopSlotFill(world): shops_per_sphere.append(current_shops_slots) candidates_per_sphere.append(current_candidates) for location in sphere: - if location.shop_slot is not None: + if isinstance(location, ALttPLocation) and location.shop_slot is not None: if not location.shop_slot_disabled: current_shops_slots.append(location) - elif not location.locked and not location.item.name in blacklist_words: + elif not location.locked and location.item.name not in blacklist_words: current_candidates.append(location) if cumu_weights: x = cumu_weights[-1] diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index b933c0740d..f54ab16e92 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -6,14 +6,19 @@ from BaseClasses import Location, Item, ItemClassification class ALttPLocation(Location): game: str = "A Link to the Past" + crystal: bool + player_address: Optional[int] + _hint_text: Optional[str] + shop_slot: Optional[int] = None + """If given as integer, shop_slot is the shop's inventory index.""" + shop_slot_disabled: bool = False - def __init__(self, player: int, name: str = '', address=None, crystal: bool = False, - hint_text: Optional[str] = None, parent=None, - player_address=None): + def __init__(self, player: int, name: str, address: Optional[int] = None, crystal: bool = False, + hint_text: Optional[str] = None, parent=None, player_address: Optional[int] = None): super(ALttPLocation, self).__init__(player, name, address, parent) self.crystal = crystal self.player_address = player_address - self._hint_text: str = hint_text + self._hint_text = hint_text class ALttPItem(Item): From c1e9d0ab4fb3609b5f2d774af7dc8994c479fbe8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 7 Aug 2022 18:28:50 +0200 Subject: [PATCH 05/19] WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- MultiServer.py | 165 ++++++++++++++++++++------------ Utils.py | 10 -- WebHost.py | 6 +- WebHostLib/__init__.py | 184 +++--------------------------------- WebHostLib/autolauncher.py | 4 +- WebHostLib/customserver.py | 39 ++++++-- WebHostLib/misc.py | 170 +++++++++++++++++++++++++++++++++ WebHostLib/tracker.py | 4 +- worlds/meritous/__init__.py | 1 - worlds/sm64ex/__init__.py | 4 +- worlds/v6/__init__.py | 1 - 11 files changed, 326 insertions(+), 262 deletions(-) create mode 100644 WebHostLib/misc.py diff --git a/MultiServer.py b/MultiServer.py index e8e1cc8d4c..3e502f649f 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -30,13 +30,8 @@ except ImportError: OperationalError = ConnectionError import NetUtils -from worlds.AutoWorld import AutoWorldRegister - -proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()} -from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name import Utils -from Utils import get_item_name_from_id, get_location_name_from_id, \ - version_tuple, restricted_loads, Version +from Utils import version_tuple, restricted_loads, Version from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \ SlotType @@ -126,6 +121,11 @@ class Context: stored_data: typing.Dict[str, object] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] + item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') + 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] + def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, @@ -190,8 +190,43 @@ class Context: self.stored_data = {} self.stored_data_notification_clients = collections.defaultdict(weakref.WeakSet) - # General networking + # init empty to satisfy linter, I suppose + self.gamespackage = {} + self.item_name_groups = {} + self.all_item_and_group_names = {} + self.forced_auto_forfeits = collections.defaultdict(lambda: False) + self.non_hintable_names = {} + self._load_game_data() + self._init_game_data() + + # Datapackage retrieval + def _load_game_data(self): + import worlds + self.gamespackage = worlds.network_data_package["games"] + + 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): + for game_name, game_package in self.gamespackage.items(): + for item_name, item_id in game_package["item_name_to_id"].items(): + self.item_names[item_id] = item_name + for location_name, location_id in game_package["location_name_to_id"].items(): + self.location_names[location_id] = location_name + self.all_item_and_group_names[game_name] = \ + set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) + + def item_names_for_game(self, game: str) -> typing.Dict[str, int]: + return self.gamespackage[game]["item_name_to_id"] + + def location_names_for_game(self, game: str) -> typing.Dict[str, int]: + return self.gamespackage[game]["location_name_to_id"] + + # General networking async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: if not endpoint.socket or not endpoint.socket.open: return False @@ -546,7 +581,7 @@ class Context: self.notify_all(finished_msg) if "auto" in self.forfeit_mode: forfeit_player(self, client.team, client.slot) - elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit: + elif self.forced_auto_forfeits[self.games[client.slot]]: forfeit_player(self, client.team, client.slot) if "auto" in self.collect_mode: collect_player(self, client.team, client.slot) @@ -642,9 +677,10 @@ async def on_client_connected(ctx: Context, client: Client): 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, - 'datapackage_version': network_data_package["version"], + 'datapackage_version': sum(game_data["version"] for game_data in ctx.gamespackage.values()) + if all(game_data["version"] for game_data in ctx.gamespackage.values()) else 0, 'datapackage_versions': {game: game_data["version"] for game, game_data - in network_data_package["games"].items()}, + in ctx.gamespackage.items()}, 'seed_name': ctx.seed_name, 'time': time.time(), }]) @@ -822,8 +858,8 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi send_items_to(ctx, team, target_player, new_item) logging.info('(Team #%d) %s sent %s to %s (%s)' % ( - team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id), - ctx.player_names[(team, target_player)], get_location_name_from_id(location))) + team + 1, ctx.player_names[(team, slot)], ctx.item_names[item_id], + ctx.player_names[(team, target_player)], ctx.location_names[location])) info_text = json_format_send_event(new_item, target_player) ctx.broadcast_team(team, [info_text]) @@ -838,13 +874,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): if slot in group: slots.add(group_id) - seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item] + + seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name] for finding_player, check_data in ctx.locations.items(): for location_id, (item_id, receiving_player, item_flags) in check_data.items(): if receiving_player in slots and item_id == seeked_item_id: @@ -857,7 +894,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[ def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]: - seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location] + seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location] return collect_hint_location_id(ctx, team, slot, seeked_location) @@ -874,8 +911,8 @@ def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str: text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \ - f"{lookup_any_item_id_to_name[hint.item]} is " \ - f"at {get_location_name_from_id(hint.location)} " \ + f"{ctx.item_names[hint.item]} is " \ + f"at {ctx.location_names[hint.location]} " \ f"in {ctx.player_names[team, hint.finding_player]}'s World" if hint.entrance: @@ -1133,8 +1170,8 @@ class ClientMessageProcessor(CommonCommandProcessor): forfeit_player(self.ctx, self.client.team, self.client.slot) return True elif "disabled" in self.ctx.forfeit_mode: - self.output( - "Sorry, client item releasing has been disabled on this server. You can ask the server admin for a /release") + 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: @@ -1170,7 +1207,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.remaining_mode == "enabled": remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1183,7 +1220,7 @@ class ClientMessageProcessor(CommonCommandProcessor): if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL: remaining_item_ids = get_remaining(self.ctx, self.client.team, self.client.slot) if remaining_item_ids: - self.output("Remaining items: " + ", ".join(lookup_any_item_id_to_name.get(item_id, "unknown item") + self.output("Remaining items: " + ", ".join(self.ctx.item_names[item_id] for item_id in remaining_item_ids)) else: self.output("No remaining items found.") @@ -1199,7 +1236,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_missing_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations] + texts = [f'Missing: {self.ctx.location_names[location]}' for location in locations] texts.append(f"Found {len(locations)} missing location checks") self.ctx.notify_client_multiple(self.client, texts) else: @@ -1212,7 +1249,7 @@ class ClientMessageProcessor(CommonCommandProcessor): locations = get_checked_checks(self.ctx, self.client.team, self.client.slot) if locations: - texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations] + texts = [f'Checked: {self.ctx.location_names[location]}' for location in locations] texts.append(f"Found {len(locations)} done location checks") self.ctx.notify_client_multiple(self.client, texts) else: @@ -1241,11 +1278,13 @@ class ClientMessageProcessor(CommonCommandProcessor): def _cmd_getitem(self, item_name: str) -> bool: """Cheat in an item, if it is enabled on this server""" if self.ctx.item_cheat: - world = proxy_worlds[self.ctx.games[self.client.slot]] - item_name, usable, response = get_intended_text(item_name, - world.item_names) + names = self.ctx.item_names_for_game(self.ctx.games[self.client.slot]) + item_name, usable, response = get_intended_text( + item_name, + names + ) if usable: - new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot) + new_item = NetworkItem(names[item_name], -1, self.client.slot) get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item) get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item) self.ctx.notify_all( @@ -1271,20 +1310,22 @@ class ClientMessageProcessor(CommonCommandProcessor): f"You have {points_available} points.") return True else: - world = proxy_worlds[self.ctx.games[self.client.slot]] - names = world.location_names if for_location else world.all_item_and_group_names + game = self.ctx.games[self.client.slot] + names = self.ctx.location_names_for_game(game) \ + if for_location else \ + self.ctx.all_item_and_group_names[game] hint_name, usable, response = get_intended_text(input_text, names) if usable: - if hint_name in world.hint_blacklist: + if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") hints = [] - elif not for_location and hint_name in world.item_name_groups: # item group name + elif not for_location and hint_name in self.ctx.item_name_groups[game]: # item group name hints = [] - for item in world.item_name_groups[hint_name]: - if item in world.item_name_to_id: # ensure item has an ID - hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) - elif not for_location and hint_name in world.item_names: # item name + for item_name in self.ctx.item_name_groups[game][hint_name]: + if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID + hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name)) + elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) else: # location name hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) @@ -1346,12 +1387,12 @@ class ClientMessageProcessor(CommonCommandProcessor): return False @mark_raw - def _cmd_hint(self, item: str = "") -> bool: + def _cmd_hint(self, item_name: str = "") -> bool: """Use !hint {item_name}, for example !hint Lamp to get a spoiler peek for that item. If hint costs are on, this will only give you one new result, you can rerun the command to get more in that case.""" - return self.get_hints(item) + return self.get_hints(item_name) @mark_raw def _cmd_hint_location(self, location: str = "") -> bool: @@ -1477,23 +1518,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): elif cmd == "GetDataPackage": exclusions = args.get("exclusions", []) if "games" in args: - games = {name: game_data for name, game_data in network_data_package["games"].items() + games = {name: game_data for name, game_data in ctx.gamespackage.items() if name in set(args.get("games", []))} await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": {"games": games}}]) # TODO: remove exclusions behaviour around 0.5.0 elif exclusions: exclusions = set(exclusions) - games = {name: game_data for name, game_data in network_data_package["games"].items() + games = {name: game_data for name, game_data in ctx.gamespackage.items() if name not in exclusions} - package = network_data_package.copy() - package["games"] = games + + package = {"games": games} await ctx.send_msgs(client, [{"cmd": "DataPackage", "data": package}]) else: await ctx.send_msgs(client, [{"cmd": "DataPackage", - "data": network_data_package}]) + "data": {"games": ctx.gamespackage}}]) elif client.auth: if cmd == "ConnectUpdate": @@ -1549,7 +1590,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): create_as_hint: int = int(args.get("create_as_hint", 0)) hints = [] for location in args["locations"]: - if type(location) is not int or location not in lookup_any_location_id_to_name: + if type(location) is not int: await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts', "original_cmd": cmd}]) @@ -1763,18 +1804,18 @@ class ServerCommandProcessor(CommonCommandProcessor): seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item_name) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.item_names) + item_name = " ".join(item_name) + names = self.ctx.item_names_for_game(self.ctx.games[slot]) + item_name, usable, response = get_intended_text(item_name, names) if usable: amount: int = int(amount) - new_items = [NetworkItem(world.item_name_to_id[item], -1, 0) for i in range(int(amount))] + new_items = [NetworkItem(names[item_name], -1, 0) for _ in range(int(amount))] send_items_to(self.ctx, team, slot, *new_items) send_new_items(self.ctx) self.ctx.notify_all( 'Cheat console: sending ' + ('' if amount == 1 else f'{amount} of ') + - f'"{item}" to {self.ctx.get_aliased_name(team, slot)}') + f'"{item_name}" to {self.ctx.get_aliased_name(team, slot)}') return True else: self.output(response) @@ -1787,22 +1828,22 @@ class ServerCommandProcessor(CommonCommandProcessor): """Sends an item to the specified player""" return self._cmd_send_multiple(1, player_name, *item_name) - def _cmd_hint(self, player_name: str, *item: str) -> bool: + def _cmd_hint(self, player_name: str, *item_name: str) -> bool: """Send out a hint for a player's item to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(item) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.all_item_and_group_names) + item_name = " ".join(item_name) + game = self.ctx.games[slot] + item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game]) if usable: - if item in world.item_name_groups: + if item_name in self.ctx.item_name_groups[game]: hints = [] - for item in world.item_name_groups[item]: - if item in world.item_name_to_id: # ensure item has an ID - hints.extend(collect_hints(self.ctx, team, slot, item)) + for item_name_from_group in self.ctx.item_name_groups[game][item_name]: + if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID + hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) else: # item name - hints = collect_hints(self.ctx, team, slot, item) + hints = collect_hints(self.ctx, team, slot, item_name) if hints: notify_hints(self.ctx, team, hints) @@ -1818,16 +1859,16 @@ class ServerCommandProcessor(CommonCommandProcessor): self.output(response) return False - def _cmd_hint_location(self, player_name: str, *location: str) -> bool: + def _cmd_hint_location(self, player_name: str, *location_name: str) -> bool: """Send out a hint for a player's location to their team""" seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item = " ".join(location) - world = proxy_worlds[self.ctx.games[slot]] - item, usable, response = get_intended_text(item, world.location_names) + location_name = " ".join(location_name) + location_name, usable, response = get_intended_text(location_name, + self.ctx.location_names_for_game(self.ctx.games[slot])) if usable: - hints = collect_hint_location_name(self.ctx, team, slot, item) + hints = collect_hint_location_name(self.ctx, team, slot, location_name) if hints: notify_hints(self.ctx, team, hints) else: diff --git a/Utils.py b/Utils.py index e38b13560d..8f00a91047 100644 --- a/Utils.py +++ b/Utils.py @@ -328,16 +328,6 @@ def get_options() -> dict: return get_options.options -def get_item_name_from_id(code: int) -> str: - from worlds import lookup_any_item_id_to_name - return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})') - - -def get_location_name_from_id(code: int) -> str: - from worlds import lookup_any_location_id_to_name - return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})') - - def persistent_store(category: str, key: typing.Any, value: typing.Any): path = user_path("_persistent_storage.yaml") storage: dict = persistent_load() diff --git a/WebHost.py b/WebHost.py index eb575df3e9..5d3c44592b 100644 --- a/WebHost.py +++ b/WebHost.py @@ -14,7 +14,7 @@ import Utils Utils.local_path.cached_path = os.path.dirname(__file__) -from WebHostLib import app as raw_app +from WebHostLib import register, app as raw_app from waitress import serve from WebHostLib.models import db @@ -22,14 +22,13 @@ from WebHostLib.autolauncher import autohost, autogen from WebHostLib.lttpsprites import update_sprites_lttp from WebHostLib.options import create as create_options_files -from worlds.AutoWorld import AutoWorldRegister - configpath = os.path.abspath("config.yaml") if not os.path.exists(configpath): # fall back to config.yaml in home configpath = os.path.abspath(Utils.user_path('config.yaml')) def get_app(): + register() app = raw_app if os.path.exists(configpath): import yaml @@ -43,6 +42,7 @@ def get_app(): def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]: import json import shutil + from worlds.AutoWorld import AutoWorldRegister worlds = {} data = [] for game, world in AutoWorldRegister.world_types.items(): diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index a44afc744a..b2d243c913 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -3,12 +3,11 @@ import uuid import base64 import socket -import jinja2.exceptions from pony.flask import Pony -from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory +from flask import Flask from flask_caching import Cache from flask_compress import Compress -from worlds.AutoWorld import AutoWorldRegister +from werkzeug.routing import BaseConverter from .models import * @@ -53,8 +52,6 @@ app.config["PATCH_TARGET"] = "archipelago.gg" cache = Cache(app) Compress(app) -from werkzeug.routing import BaseConverter - class B64UUIDConverter(BaseConverter): @@ -69,173 +66,16 @@ class B64UUIDConverter(BaseConverter): app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') -# has automatic patch integration -import Patch -app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types +def register(): + """Import submodules, triggering their registering on flask routing. + Note: initializes worlds subsystem.""" + # has automatic patch integration + import Patch + app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: game_name in Patch.AutoPatchRegister.patch_types -def get_world_theme(game_name: str): - if game_name in AutoWorldRegister.world_types: - return AutoWorldRegister.world_types[game_name].web.theme - return 'grass' + from WebHostLib.customserver import run_server_process + # to trigger app routing picking up on it + from . import tracker, upload, landing, check, generate, downloads, api, stats, misc - -@app.before_request -def register_session(): - session.permanent = True # technically 31 days after the last visit - if not session.get("_id", None): - session["_id"] = uuid4() # uniquely identify each session without needing a login - - -@app.errorhandler(404) -@app.errorhandler(jinja2.exceptions.TemplateNotFound) -def page_not_found(err): - return render_template('404.html'), 404 - - -# Start Playing Page -@app.route('/start-playing') -def start_playing(): - return render_template(f"startPlaying.html") - - -@app.route('/weighted-settings') -def weighted_settings(): - return render_template(f"weighted-settings.html") - - -# Player settings pages -@app.route('/games//player-settings') -def player_settings(game): - return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) - - -# Game Info Pages -@app.route('/games//info/') -def game_info(game, lang): - return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) - - -# List of supported games -@app.route('/games') -def games(): - worlds = {} - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - worlds[game] = world - return render_template("supportedGames.html", worlds=worlds) - - -@app.route('/tutorial///') -def tutorial(game, file, lang): - return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) - - -@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") - - -@app.route('/faq//') -def faq(lang): - return render_template("faq.html", lang=lang) - - -@app.route('/glossary//') -def terms(lang): - return render_template("glossary.html", lang=lang) - - -@app.route('/seed/') -def view_seed(seed: UUID): - seed = Seed.get(id=seed) - if not seed: - abort(404) - return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) - - -@app.route('/new_room/') -def new_room(seed: UUID): - seed = Seed.get(id=seed) - if not seed: - abort(404) - room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) - commit() - return redirect(url_for("host_room", room=room.id)) - - -def _read_log(path: str): - if os.path.exists(path): - with open(path, encoding="utf-8-sig") as log: - yield from log - else: - yield f"Logfile {path} does not exist. " \ - f"Likely a crash during spinup of multiworld instance or it is still spinning up." - - -@app.route('/log/') -def display_log(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - if room.owner == session["_id"]: - return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") - return "Access Denied", 403 - - -@app.route('/room/', methods=['GET', 'POST']) -def host_room(room: UUID): - room = Room.get(id=room) - if room is None: - return abort(404) - if request.method == "POST": - if room.owner == session["_id"]: - cmd = request.form["cmd"] - if cmd: - Command(room=room, commandtext=cmd) - commit() - - with db_session: - room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running - - return render_template("hostRoom.html", room=room) - - -@app.route('/favicon.ico') -def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static/static'), - 'favicon.ico', mimetype='image/vnd.microsoft.icon') - - -@app.route('/discord') -def discord(): - return redirect("https://discord.gg/archipelago") - - -@app.route('/datapackage') -@cache.cached() -def get_datapackge(): - """A pretty print version of /api/datapackage""" - from worlds import network_data_package - import json - return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") - - -@app.route('/index') -@app.route('/sitemap') -def get_sitemap(): - available_games = [] - for game, world in AutoWorldRegister.world_types.items(): - if not world.hidden: - available_games.append(game) - return render_template("siteMap.html", games=available_games) - - -from WebHostLib.customserver import run_server_process -from . import tracker, upload, landing, check, generate, downloads, api, stats # to trigger app routing picking up on it - -app.register_blueprint(api.api_endpoints) + app.register_blueprint(api.api_endpoints) diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index 6f978211fb..77445eadea 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -184,7 +184,7 @@ class MultiworldInstance(): logging.info(f"Spinning up {self.room_id}") process = multiprocessing.Process(group=None, target=run_server_process, - args=(self.room_id, self.ponyconfig), + args=(self.room_id, self.ponyconfig, get_static_server_data()), name="MultiHost") process.start() # bind after start to prevent thread sync issues with guardian. @@ -238,5 +238,5 @@ def run_guardian(): from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed -from .customserver import run_server_process +from .customserver import run_server_process, get_static_server_data from .generate import gen_game diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index f78a8eb24d..01f1fd25e5 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -9,12 +9,13 @@ import time import random import pickle import logging +import datetime import Utils -from .models import * +from .models import db_session, Room, select, commit, Command, db from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor -from Utils import get_public_ipv4, get_public_ipv6, restricted_loads +from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless class CustomClientMessageProcessor(ClientMessageProcessor): @@ -39,7 +40,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor): import MultiServer MultiServer.client_message_processor = CustomClientMessageProcessor -del (MultiServer) +del MultiServer class DBCommandProcessor(ServerCommandProcessor): @@ -48,12 +49,20 @@ class DBCommandProcessor(ServerCommandProcessor): class WebHostContext(Context): - def __init__(self): + def __init__(self, static_server_data: dict): + # static server data is used during _load_game_data to load required data, + # without needing to import worlds system, which takes quite a bit of memory + self.static_server_data = static_server_data super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2) + del self.static_server_data self.main_loop = asyncio.get_running_loop() self.video = {} self.tags = ["AP", "WebHost"] + def _load_game_data(self): + for key, value in self.static_server_data.items(): + setattr(self, key, value) + def listen_to_db_commands(self): cmdprocessor = DBCommandProcessor(self) @@ -107,14 +116,32 @@ def get_random_port(): return random.randint(49152, 65535) -def run_server_process(room_id, ponyconfig: dict): +@cache_argsless +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 + worlds.AutoWorldRegister.world_types.items()}, + } + + 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 + + +def run_server_process(room_id, ponyconfig: dict, static_server_data: dict): # establish DB connection for multidata and multisave db.bind(**ponyconfig) db.generate_mapping(check_tables=False) async def main(): Utils.init_logging(str(room_id), write_mode="a") - ctx = WebHostContext() + ctx = WebHostContext(static_server_data) ctx.load(room_id) ctx.init_save() diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py new file mode 100644 index 0000000000..f113c04645 --- /dev/null +++ b/WebHostLib/misc.py @@ -0,0 +1,170 @@ +import datetime +import os + +import jinja2.exceptions +from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory + +from .models import count, Seed, commit, Room, db_session, Command, UUID, uuid4 +from worlds.AutoWorld import AutoWorldRegister +from . import app, cache + + +def get_world_theme(game_name: str): + if game_name in AutoWorldRegister.world_types: + return AutoWorldRegister.world_types[game_name].web.theme + return 'grass' + + +@app.before_request +def register_session(): + session.permanent = True # technically 31 days after the last visit + if not session.get("_id", None): + session["_id"] = uuid4() # uniquely identify each session without needing a login + + +@app.errorhandler(404) +@app.errorhandler(jinja2.exceptions.TemplateNotFound) +def page_not_found(err): + return render_template('404.html'), 404 + + +# Start Playing Page +@app.route('/start-playing') +def start_playing(): + return render_template(f"startPlaying.html") + + +@app.route('/weighted-settings') +def weighted_settings(): + return render_template(f"weighted-settings.html") + + +# Player settings pages +@app.route('/games//player-settings') +def player_settings(game): + return render_template(f"player-settings.html", game=game, theme=get_world_theme(game)) + + +# Game Info Pages +@app.route('/games//info/') +def game_info(game, lang): + return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) + + +# List of supported games +@app.route('/games') +def games(): + worlds = {} + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + worlds[game] = world + return render_template("supportedGames.html", worlds=worlds) + + +@app.route('/tutorial///') +def tutorial(game, file, lang): + return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) + + +@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") + + +@app.route('/faq//') +def faq(lang): + return render_template("faq.html", lang=lang) + + +@app.route('/glossary//') +def terms(lang): + return render_template("glossary.html", lang=lang) + + +@app.route('/seed/') +def view_seed(seed: UUID): + seed = Seed.get(id=seed) + if not seed: + abort(404) + return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots)) + + +@app.route('/new_room/') +def new_room(seed: UUID): + seed = Seed.get(id=seed) + if not seed: + abort(404) + room = Room(seed=seed, owner=session["_id"], tracker=uuid4()) + commit() + return redirect(url_for("host_room", room=room.id)) + + +def _read_log(path: str): + if os.path.exists(path): + with open(path, encoding="utf-8-sig") as log: + yield from log + else: + yield f"Logfile {path} does not exist. " \ + f"Likely a crash during spinup of multiworld instance or it is still spinning up." + + +@app.route('/log/') +def display_log(room: UUID): + room = Room.get(id=room) + if room is None: + return abort(404) + if room.owner == session["_id"]: + return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") + return "Access Denied", 403 + + +@app.route('/room/', methods=['GET', 'POST']) +def host_room(room: UUID): + room = Room.get(id=room) + if room is None: + return abort(404) + if request.method == "POST": + if room.owner == session["_id"]: + cmd = request.form["cmd"] + if cmd: + Command(room=room, commandtext=cmd) + commit() + + with db_session: + room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running + + return render_template("hostRoom.html", room=room) + + +@app.route('/favicon.ico') +def favicon(): + return send_from_directory(os.path.join(app.root_path, 'static/static'), + 'favicon.ico', mimetype='image/vnd.microsoft.icon') + + +@app.route('/discord') +def discord(): + return redirect("https://discord.gg/archipelago") + + +@app.route('/datapackage') +@cache.cached() +def get_datapackge(): + """A pretty print version of /api/datapackage""" + from worlds import network_data_package + import json + return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain") + + +@app.route('/index') +@app.route('/sitemap') +def get_sitemap(): + available_games = [] + for game, world in AutoWorldRegister.world_types.items(): + if not world.hidden: + available_games.append(game) + return render_template("siteMap.html", games=available_games) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5e249c19ea..4179478985 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -11,7 +11,7 @@ from worlds.alttp import Items from WebHostLib import app, cache, Room from Utils import restricted_loads from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name -from MultiServer import get_item_name_from_id, Context +from MultiServer import Context from NetUtils import SlotType alttp_icons = { @@ -1021,7 +1021,7 @@ def getTracker(tracker: UUID): for (team, player), data in multisave.get("video", []): video[(team, player)] = data - return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id, + return render_template("tracker.html", inventory=inventory, get_item_name_from_id=lookup_any_item_id_to_name, lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names, tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=alttp_icons, multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas, diff --git a/worlds/meritous/__init__.py b/worlds/meritous/__init__.py index 3c29032aa5..3a98bfe562 100644 --- a/worlds/meritous/__init__.py +++ b/worlds/meritous/__init__.py @@ -45,7 +45,6 @@ class MeritousWorld(World): item_name_groups = item_groups data_version = 2 - forced_auto_forfeit = False # NOTE: Remember to change this before this game goes live required_client_version = (0, 2, 4) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 401a2d683b..46282fe316 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -35,9 +35,7 @@ class SM64World(World): location_name_to_id = location_table data_version = 6 - required_client_version = (0,3,0) - - forced_auto_forfeit = False + required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 04947716d3..4959ddca1b 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -35,7 +35,6 @@ class V6World(World): location_name_to_id = location_table data_version = 1 - forced_auto_forfeit = False area_connections: typing.Dict[int, int] area_cost_map: typing.Dict[int,int] From eb5ba72cfc95e17b148c267fc24f94eeee3e5ba2 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:23:22 -0400 Subject: [PATCH 06/19] Smz3 min accessibility fix (#880) --- worlds/smz3/TotalSMZ3/Config.py | 1 - worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py | 3 +-- worlds/smz3/__init__.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/worlds/smz3/TotalSMZ3/Config.py b/worlds/smz3/TotalSMZ3/Config.py index 1c3bddb188..bfcd541b98 100644 --- a/worlds/smz3/TotalSMZ3/Config.py +++ b/worlds/smz3/TotalSMZ3/Config.py @@ -48,7 +48,6 @@ class Config: Keysanity: bool = KeyShuffle != KeyShuffle.Null Race: bool = False GanonInvincible: GanonInvincible = GanonInvincible.BeforeCrystals - MinimalAccessibility: bool = False # AP specific accessibility: minimal def __init__(self, options: Dict[str, str]): self.GameMode = self.ParseOption(options, GameMode.Multiworld) diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index ab7c86a631..1805e74dca 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -14,8 +14,7 @@ class SwampPalace(Z3Region, IReward): self.Reward = RewardType.Null self.Locations = [ Location(self, 256+135, 0x1EA9D, LocationType.Regular, "Swamp Palace - Entrance") - .Allow(lambda item, items: self.Config.Keysanity or self.Config.MinimalAccessibility or - item.Is(ItemType.KeySP, self.world)), + .Allow(lambda item, items: self.Config.Keysanity or item.Is(ItemType.KeySP, self.world)), Location(self, 256+136, 0x1E986, LocationType.Regular, "Swamp Palace - Map Chest", lambda items: items.KeySP), Location(self, 256+137, 0x1E989, LocationType.Regular, "Swamp Palace - Big Chest", diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index fcedfd45db..9a0fcad90e 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -190,7 +190,6 @@ class SMZ3World(World): config.KeyShuffle = KeyShuffle(self.world.key_shuffle[self.player].value) config.Keysanity = config.KeyShuffle != KeyShuffle.Null config.GanonInvincible = GanonInvincible.BeforeCrystals - config.MinimalAccessibility = self.world.accessibility[self.player] == Accessibility.option_minimal self.local_random = random.Random(self.world.random.randint(0, 1000)) self.smz3World = TotalSMZ3World(config, self.world.get_player_name(self.player), self.player, self.world.seed_name) @@ -525,6 +524,7 @@ class SMZ3World(World): def InitialFillInOwnWorld(self): self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySW, self.smz3World.GetLocation("Skull Woods - Pinball Room")) + self.FillItemAtLocation(self.dungeon, TotalSMZ3Item.ItemType.KeySP, self.smz3World.GetLocation("Swamp Palace - Entrance")) # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: From a378d62dfd2a93d14b14253e46fed7fab57ab969 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 8 Aug 2022 23:20:18 +0200 Subject: [PATCH 07/19] SC2: fix Moebius Factor rescue condition (#882) --- worlds/sc2wol/Locations.py | 12 ++++++------ worlds/sc2wol/LogicMixin.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index dc2ec74a4b..92dfb033c0 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -103,19 +103,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, - lambda state: state._sc2wol_has_air(world, player) or True), + lambda state: True), LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Mid Rescue", SC2WOL_LOC_ID_OFFSET + 1005, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Nydus Roof Rescue", SC2WOL_LOC_ID_OFFSET + 1006, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007, - lambda state: state._sc2wol_able_to_rescue(world, player) or True), + lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008, lambda state: state._sc2wol_has_air(world, player)), LocationData("The Moebius Factor", "Beat The Moebius Factor", None, diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index 7e2fc2f0e8..baf77dc677 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -37,7 +37,7 @@ class SC2WoLLogic(LogicMixin): self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Medivac', 'Hercules', 'Raven', 'Orbital Strike'}, player) + return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) From fb2979d9ef084bc02d630e7b0966bfdd525919c6 Mon Sep 17 00:00:00 2001 From: TheCondor07 Date: Mon, 8 Aug 2022 15:20:51 -0700 Subject: [PATCH 08/19] SC2: Added Difficulty Override to Client (#863) --- Starcraft2Client.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index e9e06335ac..dc63e9a456 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -44,11 +44,38 @@ nest_asyncio.apply() class StarcraftClientProcessor(ClientCommandProcessor): ctx: SC2Context + def _cmd_difficulty(self, difficulty: str = "") -> bool: + """Overrides the current difficulty set for the seed. Takes the argument casual, normal, hard, or brutal""" + options = difficulty.split() + num_options = len(options) + difficulty_choice = options[0].lower() + + if num_options > 0: + if difficulty_choice == "casual": + self.ctx.difficulty_override = 0 + elif difficulty_choice == "normal": + self.ctx.difficulty_override = 1 + elif difficulty_choice == "hard": + self.ctx.difficulty_override = 2 + elif difficulty_choice == "brutal": + self.ctx.difficulty_override = 3 + else: + self.output("Unable to parse difficulty '" + options[0] + "'") + return False + + self.output("Difficulty set to " + options[0]) + return True + + else: + self.output("Difficulty needs to be specified in the command.") + return False + def _cmd_disable_mission_check(self) -> bool: """Disables the check to see if a mission is available to play. Meant for co-op runs where one player can play the next mission in a chain the other player is doing.""" self.ctx.missions_unlocked = True sc2_logger.info("Mission check has been disabled") + return True def _cmd_play(self, mission_id: str = "") -> bool: """Start a Starcraft 2 mission""" @@ -64,6 +91,7 @@ class StarcraftClientProcessor(ClientCommandProcessor): else: sc2_logger.info( "Mission ID needs to be specified. Use /unfinished or /available to view ids for available missions.") + return False return True @@ -108,6 +136,7 @@ class SC2Context(CommonContext): missions_unlocked = False current_tooltip = None last_loc_list = None + difficulty_override = -1 async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -470,7 +499,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): game_state = 0 if iteration == 0: start_items = calculate_items(self.ctx.items_received) - difficulty = calc_difficulty(self.ctx.difficulty) + if self.ctx.difficulty_override >= 0: + difficulty = calc_difficulty(self.ctx.difficulty_override) + else: + difficulty = calc_difficulty(self.ctx.difficulty) await self.chat_send("ArchipelagoLoad {} {} {} {} {} {} {} {} {} {} {} {} {}".format( difficulty, start_items[0], start_items[1], start_items[2], start_items[3], start_items[4], From b3700dabf20ba1890ef49130fbd5c1bfe7b86e5d Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 8 Aug 2022 19:29:00 -0500 Subject: [PATCH 09/19] Core: Fix meta.yaml and allow the `None` game category for common options (#845) --- Generate.py | 54 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/Generate.py b/Generate.py index 7cfe7a504c..70a8eaf667 100644 --- a/Generate.py +++ b/Generate.py @@ -7,7 +7,7 @@ import urllib.request import urllib.parse from typing import Set, Dict, Tuple, Callable, Any, Union import os -from collections import Counter +from collections import Counter, ChainMap import string import enum @@ -133,12 +133,14 @@ def main(args=None, callback=ERmain): if args.meta_file_path and os.path.exists(args.meta_file_path): try: - weights_cache[args.meta_file_path] = read_weights_yamls(args.meta_file_path) + meta_weights = read_weights_yamls(args.meta_file_path)[-1] except Exception as e: raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e - meta_weights = weights_cache[args.meta_file_path][-1] print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}") - del(meta_weights["meta_description"]) + try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file + del(meta_weights["meta_description"]) + except Exception as e: + raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e if args.samesettings: raise Exception("Cannot mix --samesettings with --meta") else: @@ -164,7 +166,7 @@ def main(args=None, callback=ERmain): player_files[player_id] = filename player_id += 1 - args.multi = max(player_id-1, args.multi) + args.multi = max(player_id - 1, args.multi) print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: " f"{args.plando}") @@ -186,26 +188,28 @@ def main(args=None, callback=ERmain): erargs.enemizercli = args.enemizercli settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ - {fname: ( tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) - for fname, yamls in weights_cache.items()} - player_path_cache = {} - for player in range(1, args.multi + 1): - player_path_cache[player] = player_files.get(player, args.weights_file_path) + {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) + for fname, yamls in weights_cache.items()} if meta_weights: for category_name, category_dict in meta_weights.items(): for key in category_dict: - option = get_choice(key, category_dict) + option = roll_meta_option(key, category_name, category_dict) if option is not None: - for player, path in player_path_cache.items(): + for path in weights_cache: for yaml in weights_cache[path]: if category_name is None: - yaml[key] = option + for category in yaml: + if category in AutoWorldRegister.world_types and key in Options.common_options: + yaml[category][key] = option elif category_name not in yaml: logging.warning(f"Meta: Category {category_name} is not present in {path}.") else: - yaml[category_name][key] = option + yaml[category_name][key] = option + player_path_cache = {} + for player in range(1, args.multi + 1): + player_path_cache[player] = player_files.get(player, args.weights_file_path) name_counter = Counter() erargs.player_settings = {} @@ -387,6 +391,28 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di return weights +def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: + if not game: + return get_choice(option_key, category_dict) + if game in AutoWorldRegister.world_types: + game_world = AutoWorldRegister.world_types[game] + options = ChainMap(game_world.options, Options.per_game_common_options) + if option_key in options: + if options[option_key].supports_weighting: + return get_choice(option_key, category_dict) + return options[option_key] + if game == "A Link to the Past": # TODO wow i hate this + if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode", + "triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra", + "triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality", + "boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time", + "red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes", + "misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite", + "random_sprite_on_event"}: + return get_choice(option_key, category_dict) + raise Exception(f"Error generating meta option {option_key} for {game}.") + + def roll_linked_options(weights: dict) -> dict: weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings for option_set in weights["linked_options"]: From 2c4e819010df33be20262b707a56636492d9d866 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 9 Aug 2022 03:47:01 -0500 Subject: [PATCH 10/19] docs: plando update (#861) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/generic/docs/plando_en.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index c73fa8d398..fa3edd1fa1 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -7,8 +7,7 @@ changes it up by allowing you to plan out certain aspects of the game by placing certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`, and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported -by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss -plando. +by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary. ### Enabling Plando From debda5d11149d18a5a278c2704f0ab7959e95344 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 9 Aug 2022 06:21:05 +0200 Subject: [PATCH 11/19] MultiServer: swap auto-forfeit with auto-collect order That way the forfeit for items for players that are still playing appear last in the log, which is the visible text in at least the py clients --- MultiServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 3e502f649f..8a1844bf92 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -579,12 +579,12 @@ class Context: finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \ f' has completed their goal.' 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.collect_mode: - collect_player(self, client.team, client.slot) def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False): From f2e83c37e9e4e4fdd1de9d63e9d7692859bbcb75 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 9 Aug 2022 22:21:45 +0200 Subject: [PATCH 12/19] WebHost: use title-typical sorting for game titles (#883) --- Utils.py | 11 +++++++++++ WebHost.py | 2 +- WebHostLib/__init__.py | 2 ++ WebHostLib/templates/supportedGames.html | 3 ++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Utils.py b/Utils.py index 8f00a91047..82d13b3f84 100644 --- a/Utils.py +++ b/Utils.py @@ -603,3 +603,14 @@ def messagebox(title: str, text: str, error: bool = False) -> None: root.withdraw() showerror(title, text) if error else showinfo(title, text) root.update() + + +def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))): + """Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning.""" + def sorter(element: str) -> str: + parts = element.split(maxsplit=1) + if parts[0].lower() in ignore: + return parts[1] + else: + return element + return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) diff --git a/WebHost.py b/WebHost.py index 5d3c44592b..09f8d8235a 100644 --- a/WebHost.py +++ b/WebHost.py @@ -85,7 +85,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] for games in data: if 'Archipelago' in games['gameTitle']: generic_data = data.pop(data.index(games)) - sorted_data = [generic_data] + sorted(data, key=lambda entry: entry["gameTitle"].lower()) + sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower()) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) return sorted_data diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index b2d243c913..b7bf4e38d1 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -9,6 +9,7 @@ from flask_caching import Cache from flask_compress import Compress from werkzeug.routing import BaseConverter +from Utils import title_sorted from .models import * UPLOAD_FOLDER = os.path.relpath('uploads') @@ -65,6 +66,7 @@ class B64UUIDConverter(BaseConverter): # short UUID app.url_map.converters["suuid"] = B64UUIDConverter app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii') +app.jinja_env.filters["title_sorted"] = title_sorted def register(): diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index 125551fbb9..fe81463a46 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -10,7 +10,8 @@ {% include 'header/oceanHeader.html' %}

Currently Supported Games

- {% for game_name, world in worlds.items() | sort(attribute=0) %} + {% for game_name in worlds | title_sorted %} + {% set world = worlds[game_name] %}

{{ game_name }}

{{ world.__doc__ | default("No description provided.", true) }}
From e1e2526322aeaf6b59bf37ffa892c547762faf3d Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Wed, 10 Aug 2022 13:21:52 -0700 Subject: [PATCH 13/19] LttP: Do a check for enemizer much earlier in generation. (#875) --- worlds/alttp/__init__.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 64b1bf8db5..8f39b606e4 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -14,8 +14,8 @@ from .Rules import set_rules from .ItemPool import generate_itempool, difficulties from .Shops import create_shops, ShopSlotFill from .Dungeons import create_dungeons -from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string, \ - get_base_rom_path, LttPDeltaPatch +from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ + get_hash_string, get_base_rom_path, LttPDeltaPatch import Patch from itertools import chain @@ -156,6 +156,9 @@ class ALTTPWorld(World): player = self.player world = self.world + if self.use_enemizer(): + check_enemizer(world.enemizer) + # system for sharing ER layouts self.er_seed = str(world.random.randint(0, 2 ** 64)) @@ -341,14 +344,19 @@ class ALTTPWorld(World): def stage_post_fill(cls, world): ShopSlotFill(world) + def use_enemizer(self): + world = self.world + player = self.player + return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] + or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' + or world.pot_shuffle[player] or world.bush_shuffle[player] + or world.killable_thieves[player]) + def generate_output(self, output_directory: str): world = self.world player = self.player try: - use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] - or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' - or world.pot_shuffle[player] or world.bush_shuffle[player] - or world.killable_thieves[player]) + use_enemizer = self.use_enemizer() rom = LocalRom(get_base_rom_path()) From 29e09758324a757abc4fac126c2ee5bdb37001c3 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 10 Aug 2022 22:20:14 +0200 Subject: [PATCH 14/19] Clients: prepare for removal of players key in RoomInfo --- CommonClient.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 76623ff3f2..e0a9c30784 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -562,18 +562,21 @@ async def process_server_cmd(ctx: CommonContext, args: dict): f" for each location checked. Use !hint for more information.") ctx.hint_cost = int(args['hint_cost']) ctx.check_points = int(args['location_check_points']) - players = args.get("players", []) - if len(players) < 1: - logger.info('No player connected') - else: - players.sort() - current_team = -1 - logger.info('Connected Players:') - for network_player in players: - if network_player.team != current_team: - logger.info(f' Team #{network_player.team + 1}') - current_team = network_player.team - logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) + + if "players" in args: # TODO remove when servers sending this are outdated + players = args.get("players", []) + if len(players) < 1: + logger.info('No player connected') + else: + players.sort() + current_team = -1 + logger.info('Connected Players:') + for network_player in players: + if network_player.team != current_team: + logger.info(f' Team #{network_player.team + 1}') + current_team = network_player.team + logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot)) + # update datapackage await ctx.prepare_datapackage(set(args["games"]), args["datapackage_versions"]) From b98969874019e14baaeb3793d420a6b40e84c404 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 11 Aug 2022 00:58:08 +0200 Subject: [PATCH 15/19] WebHost: fix datapackage typo --- WebHostLib/api/__init__.py | 4 ++-- WebHostLib/misc.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index c2f9b3840f..80c60a093a 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -32,14 +32,14 @@ def room_info(room: UUID): @api_endpoints.route('/datapackage') @cache.cached() -def get_datapackge(): +def get_datapackage(): from worlds import network_data_package return network_data_package @api_endpoints.route('/datapackage_version') @cache.cached() -def get_datapackge_versions(): +def get_datapackage_versions(): from worlds import network_data_package, AutoWorldRegister version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()} version_package["version"] = network_data_package["version"] diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index f113c04645..44377cf445 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -153,7 +153,7 @@ def discord(): @app.route('/datapackage') @cache.cached() -def get_datapackge(): +def get_datapackage(): """A pretty print version of /api/datapackage""" from worlds import network_data_package import json From ffe528467e4735d858d00c412166638671703c5a Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 11 Aug 2022 01:02:06 +0200 Subject: [PATCH 16/19] Generate: remove period for easy copy&paste Double-clicking in terminal may select the period, resulting in a bad filename in clipboard. Also fixing quotes. --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 3b094326f2..48095e06bd 100644 --- a/Main.py +++ b/Main.py @@ -423,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) zipfilename = output_path(f"AP_{world.seed_name}.zip") - logger.info(f'Creating final archive at {zipfilename}.') + logger.info(f"Creating final archive at {zipfilename}") with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zf: for file in os.scandir(temp_dir): From c96acbfa2379f8a977fb3ecaa681799b65fb90a7 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 10 Aug 2022 23:05:36 +0200 Subject: [PATCH 17/19] TextClient: receive all items By popular demand, this makes /received work again. Closes #887 --- CommonClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index e0a9c30784..0b2c22cfd8 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -726,7 +726,7 @@ if __name__ == '__main__': class TextContext(CommonContext): tags = {"AP", "IgnoreGame", "TextOnly"} game = "" # empty matches any game since 0.3.2 - items_handling = 0 # don't receive any NetworkItems + items_handling = 0b111 # receive all items for /received async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: From b32d0efe6dcb69f6bbd887efdbf195ca8eb5784a Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 11 Aug 2022 15:57:33 +0200 Subject: [PATCH 18/19] Witness: Logic fix for Treehouse in Doors (#892) --- worlds/witness/WitnessLogic.txt | 10 +++++++--- worlds/witness/player_logic.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index 71ca7c819f..c98257fb73 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -691,7 +691,7 @@ Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17 158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares 158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares -Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323: +Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB: 158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol @@ -707,8 +707,6 @@ Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room - 0x0C323: 158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol 158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158611 - 0x17FA0 (Burnt House Discard) - 0x17DDB - Triangles -Door - 0x0C323 (Door to Laser House) - 0x17DDB & 0x17DA2 & 0x2700B Treehouse Green Bridge (Treehouse): 158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers @@ -720,6 +718,12 @@ Treehouse Green Bridge (Treehouse): 158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers 158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles +Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: +Door - 0x0C323 (Door to Laser House) - 0x17DA2 & 0x2700B & 0x17DDB + +Treehouse Laser Room Back Platform (Treehouse): +158611 - 0x17FA0 (Burnt House Discard) - True - Triangles + Treehouse Laser Room (Treehouse): 158712 - 0x03613 (Laser Panel) - True - True 158403 - 0x17CBC (Laser House Door Timer Inside Control) - True - True diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 3639e836df..efbb177f00 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -355,6 +355,7 @@ class WitnessPlayerLogic: "0x19B24": "Shadows Lower Avoid Patterns Visible", "0x2700B": "Open Door to Treehouse Laser House", "0x00055": "Orchard Apple Trees 4 Turns On", + "0x17DDB": "Left Orange Bridge Fully Extended", } self.ALWAYS_EVENT_NAMES_BY_HEX = { From adc16fdd3dad1827e9df769561ce8c3b8023d70a Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Thu, 11 Aug 2022 09:11:34 -0700 Subject: [PATCH 19/19] Factorio: Don't send researches completed by editor extensions testing forces. (#894) --- worlds/factorio/data/mod_template/control.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index e7774fef00..63473808c6 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -286,6 +286,12 @@ end) -- hook into researches done script.on_event(defines.events.on_research_finished, function(event) local technology = event.research + if string.find(technology.force.name, "EE_TESTFORCE") == 1 then + --Don't acknowledge AP research as an Editor Extensions test force + --Also no need for free samples in the Editor extensions testing surfaces, as these testing surfaces + --are worked on exclusively in editor mode. + return + end if technology.researched and string.find(technology.name, "ap%-") == 1 then -- check if it came from the server anyway, then we don't need to double send. dumpInfo(technology.force) --is sendable