From 76d591bab5982d57a7827f021f7f8ccc551aad8d Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Fri, 8 Oct 2021 17:20:05 -0400 Subject: [PATCH 01/27] Update adding games.md --- docs/adding games.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adding games.md b/docs/adding games.md index d426b9ce..0a76cb66 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -2,7 +2,7 @@ # How do I add a game to Archipelago? This guide is going to try and be a broad summary of how you can do just that. -There are three key steps to incorporating a game into Archipelago: +There are two key steps to incorporating a game into Archipelago: - Game Modification - Archipelago Server Integration From 6acd08431e161193b6dac7b3352ef5d016e132dd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 9 Oct 2021 02:30:46 +0200 Subject: [PATCH 02/27] Core: fix set_seed seed passthrough --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 8e26a1e5..7b29a440 100644 --- a/Main.py +++ b/Main.py @@ -30,7 +30,7 @@ def main(args, seed=None): world = MultiWorld(args.multi) logger = logging.getLogger() - world.set_seed(secure=args.race, name=str(args.outputname if args.outputname else world.seed)) + world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed)) world.shuffle = args.shuffle.copy() world.logic = args.logic.copy() From 62db9ad982f6c688f70dc479d1509d9410d4a5a1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 9 Oct 2021 15:24:08 +0200 Subject: [PATCH 03/27] MultiServer: send RoomUpdate -> permissions if permissions change --- MultiServer.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 28a510b9..5ad4654b 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -26,6 +26,7 @@ from prompt_toolkit.patch_stdout import patch_stdout from fuzzywuzzy import process as fuzzy_process 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 @@ -293,7 +294,7 @@ class Context: if not self.save_filename: import os name, ext = os.path.splitext(self.data_filename) - self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \ + self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \ else self.data_filename + '_' + 'apsave' try: with open(self.save_filename, 'rb') as f: @@ -472,10 +473,7 @@ async def on_client_connected(ctx: Context, client: Client): # TODO ~0.2.0 remove forfeit_mode and remaining_mode in favor of permissions 'forfeit_mode': ctx.forfeit_mode, 'remaining_mode': ctx.remaining_mode, - 'permissions': { - "forfeit": Permission.from_text(ctx.forfeit_mode), - "remaining": Permission.from_text(ctx.remaining_mode), - }, + 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, 'datapackage_version': network_data_package["version"], @@ -485,6 +483,13 @@ 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), + "remaining": Permission.from_text(ctx.remaining_mode), + } + + async def on_client_disconnected(ctx: Context, client: Client): if client.auth: await on_client_left(ctx, client) @@ -1181,7 +1186,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): locs = [] for location in args["locations"]: if type(location) is not int or location not in lookup_any_location_id_to_name: - await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}]) + await ctx.send_msgs(client, + [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}]) return target_item, target_player = ctx.locations[client.slot][location] locs.append(NetworkItem(target_item, location, target_player)) @@ -1407,6 +1413,8 @@ class ServerCommandProcessor(CommonCommandProcessor): 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"}: + self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}]) return True else: known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items()) From b1fb793ea4d358afa2064989be241e520a28d399 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 9 Oct 2021 08:57:37 -0500 Subject: [PATCH 04/27] Ror2: fix generation mistake (#100) * Risk of Rain 2: logic updates * Risk of Rain 2: move a variable definition so it can be reused. Reverted a change that broke stuff for some reason. * Documentation update --- worlds/ror2/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 329dc4c5..3a4611d7 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -32,8 +32,8 @@ class RiskOfRainWorld(World): self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player)) # if presets are enabled generate junk_pool from the selected preset + pool_option = self.world.item_weights[self.player].value if self.world.item_pool_presets[self.player].value: - pool_option = self.world.item_weights[self.player].value # generate chaos weights if the preset is chosen if pool_option == 5: junk_pool = { @@ -104,13 +104,13 @@ class RiskOfRainWorld(World): item = RiskOfRainItem(name, True, item_id, self.player) return item - +# generate locations based on player setting def create_regions(world, player: int): world.regions += [ create_region(world, player, 'Menu', None, ['Lobby']), create_region(world, player, 'Petrichor V', [location for location in base_location_table] + - [f"Item Pickup {i}" for i in range(1, world.start_with_revive[player].value+world.total_locations[player])]) + [f"Item Pickup {i}" for i in range(1, 1 + world.total_locations[player])]) ] world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player)) From c7a315ac97fbafba70876744abf3526f7c51c4e8 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 9 Oct 2021 11:58:38 +0200 Subject: [PATCH 05/27] Refactorings --- worlds/timespinner/Locations.py | 2 +- worlds/timespinner/Regions.py | 8 ++++---- worlds/timespinner/__init__.py | 23 +++++++++++------------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 7e0a884d..8acd58c4 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -10,7 +10,7 @@ class LocationData(NamedTuple): code: Optional[int] rule: Callable = lambda state: True -def get_locations(world: Optional[MultiWorld], player: Optional[int]): +def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: location_table: Tuple[LocationData, ...] = ( # PresentItemLocations LocationData('Tutorial', 'Yo Momma 1', 1337000), diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 863f7339..12782821 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -150,9 +150,9 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Space time continuum', 'Caves of Banishment (upper)', lambda state: pyramid_keys_unlock == "GateCavesOfBanishment") -def create_location(player: int, name: str, id: Optional[int], region: Region, rule: Callable, location_cache: List[Location]) -> Location: - location = Location(player, name, id, region) - location.access_rule = rule +def create_location(player: int, location_data: LocationData, region: Region, location_cache: List[Location]) -> Location: + location = Location(player, location_data.name, location_data.code, region) + location.access_rule = location_data.rule if id is None: location.event = True @@ -169,7 +169,7 @@ def create_region(world: MultiWorld, player: int, locations_per_region: Dict[str if name in locations_per_region: for location_data in locations_per_region[name]: - location = create_location(player, location_data.name, location_data.code, region, location_data.rule, location_cache) + location = create_location(player, location_data, region, location_cache) region.locations.append(location) return region diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cf7e75dc..dae726ba 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -84,25 +84,25 @@ def create_item(name: str, player: int) -> Item: return Item(name, data.progression, data.code, player) -def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> List[str]: - excluded_items: List[str] = [] +def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]: + excluded_items: Set[str] = [] if is_option_enabled(world, player, "StartWithJewelryBox"): - excluded_items.append('Jewelry Box') + excluded_items.add('Jewelry Box') if is_option_enabled(world, player, "StartWithMeyef"): - excluded_items.append('Meyef') + excluded_items.add('Meyef') if is_option_enabled(world, player, "QuickSeed"): - excluded_items.append('Talaria Attachment') + excluded_items.add('Talaria Attachment') return excluded_items -def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[str], locked_locations: List[str]): +def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): melee_weapon = world.random.choice(starter_melee_weapons) spell = world.random.choice(starter_spells) - excluded_items.append(melee_weapon) - excluded_items.append(spell) + excluded_items.add(melee_weapon) + excluded_items.add(spell) melee_weapon_item = create_item(melee_weapon, player) spell_item = create_item(spell, player) @@ -114,7 +114,7 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: List[st locked_locations.append('Yo Momma 2') -def get_item_pool(world: MultiWorld, player: int, excluded_items: List[str]) -> List[Item]: +def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]: pool: List[Item] = [] for name, data in item_table.items(): @@ -132,12 +132,11 @@ def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locat pool.append(item) -def place_first_progression_item(world: MultiWorld, player: int, excluded_items: List[str], - locked_locations: List[str]): +def place_first_progression_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]): progression_item = world.random.choice(starter_progression_items) location = world.random.choice(starter_progression_locations) - excluded_items.append(progression_item) + excluded_items.add(progression_item) locked_locations.append(location) item = create_item(progression_item, player) From ba13d2179d3a82e4693cdd3a6ed50d08b3eca871 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 9 Oct 2021 15:40:08 +0200 Subject: [PATCH 06/27] Slightly improved docs about permissions flags --- docs/network protocol.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index c4326a7c..f55435c0 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -53,7 +53,7 @@ Sent to clients when they connect to an Archipelago server. | version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | | password | bool | Denoted whether a password is required to join this room.| -| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to Permission, known names: "forfeit" and "remaining". | +| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit" and "remaining". | | hint_cost | int | The amount of points it costs to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. || | players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. | From b539892cc0561eb188dd0821e36321339f7d1830 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 9 Oct 2021 15:53:18 +0200 Subject: [PATCH 07/27] Fixed Timespinner generation *oops* --- worlds/timespinner/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index dae726ba..e58d0d92 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -85,7 +85,7 @@ def create_item(name: str, player: int) -> Item: def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]: - excluded_items: Set[str] = [] + excluded_items: Set[str] = set() if is_option_enabled(world, player, "StartWithJewelryBox"): excluded_items.add('Jewelry Box') From eb602aedc30a486e4d306a4c51dc8fae191331f8 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sat, 9 Oct 2021 11:07:48 -0500 Subject: [PATCH 08/27] Fill overworld-shuffle dungeon items with logic Prevents maps and compasses from failing fast fill --- worlds/oot/__init__.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index dcf7a1d9..5e7affb0 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -586,14 +586,20 @@ class OOTWorld(World): fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations, itempools['any_dungeon'], True, True) - # If anything is overworld-only, enforce them as local and not in the remaining dungeon locations - if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld': - from worlds.generic.Rules import forbid_items_for_player - fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set() - local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys) - for location in self.world.get_locations(): - if location.player != self.player or location in any_dungeon_locations: - forbid_items_for_player(location, local_overworld_items, self.player) + # If anything is overworld-only, fill into local non-dungeon locations + if self.shuffle_fortresskeys == 'overworld': + fortresskeys = filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.world.itempool) + itempools['overworld'].extend(fortresskeys) + if itempools['overworld']: + for item in itempools['overworld']: + self.world.itempool.remove(item) + itempools['overworld'].sort(key=lambda item: + {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0)) + non_dungeon_locations = [loc for loc in self.get_locations() if not loc.item and loc not in any_dungeon_locations + and loc.type != 'Shop' and (loc.type != 'Song' or self.shuffle_song_items != 'song')] + self.world.random.shuffle(non_dungeon_locations) + fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations, + itempools['overworld'], True, True) # Place songs # 5 built-in retries because this section can fail sometimes From f8deb1bd7f17b80f14d090027589a2e42509ee3f Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 9 Oct 2021 20:38:53 -0700 Subject: [PATCH 09/27] Make visible_sending part of AutoWorld. --- Main.py | 5 ++--- worlds/AutoWorld.py | 3 +++ worlds/factorio/__init__.py | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Main.py b/Main.py index 7b29a440..776438ac 100644 --- a/Main.py +++ b/Main.py @@ -252,12 +252,11 @@ def main(args, seed=None): precollected_hints = {player: set() for player in range(1, world.players + 1)} # for now special case Factorio tech_tree_information sending_visible_players = set() - for player in world.get_game_players("Factorio"): - if world.tech_tree_information[player].value == 2: - sending_visible_players.add(player) for slot in world.player_ids: slot_data[slot] = world.worlds[slot].fill_slot_data() + if world.worlds[slot].sending_visible: + sending_visible_players.add(slot) def precollect_hint(location): hint = NetUtils.Hint(location.item.player, location.player, location.address, diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index bf48fd97..47936d14 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -114,6 +114,9 @@ class World(metaclass=AutoWorldRegister): item_names: Set[str] # set of all potential item names location_names: Set[str] # set of all potential location names + # If there is visibility in what is being sent, this is where it will be known. + sending_visible: bool = False + def __init__(self, world: MultiWorld, player: int): self.world = world self.player = player diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 99dba71e..e9c779a7 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -66,6 +66,9 @@ class Factorio(World): if map_basic_settings.get("seed", None) is None: # allow seed 0 map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint + self.sending_visible = self.world.tech_tree_information[player] == Options.TechTreeInformation.option_full + + generate_output = generate_mod def create_regions(self): From ca4b0acd921d036336982e1bffb80de2c1851da5 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 9 Oct 2021 20:47:12 -0700 Subject: [PATCH 10/27] Add !hint_location command. As it turns out, because factorio location names are 100% identical to factorio item names, it is impossible without a command that explicitly hints locations to hint a specific factorio location, or any other game where location names match item names. --- MultiServer.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 5ad4654b..1520d528 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -977,14 +977,9 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output("Cheating is disabled.") return False - @mark_raw - def _cmd_hint(self, item_or_location: str = "") -> bool: - """Use !hint {item_name/location_name}, - for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or 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.""" + def get_hints(self, input_text: str, explicit_location: bool = False) -> bool: points_available = get_client_points(self.ctx, self.client) - if not item_or_location: + if not input_text: hints = {hint.re_check(self.ctx, self.client.team) for hint in self.ctx.hints[self.client.team, self.client.slot]} self.ctx.hints[self.client.team, self.client.slot] = hints @@ -994,16 +989,16 @@ class ClientMessageProcessor(CommonCommandProcessor): return True else: world = proxy_worlds[self.ctx.games[self.client.slot]] - item_name, usable, response = get_intended_text(item_or_location, world.all_names) + item_name, usable, response = get_intended_text(input_text, world.all_names if not explicit_location else world.location_names) if usable: if item_name in world.hint_blacklist: self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.") hints = [] - elif item_name in world.item_name_groups: + elif item_name in world.item_name_groups and not explicit_location: hints = [] for item in world.item_name_groups[item_name]: hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item)) - elif item_name in world.item_names: # item name + elif item_name in world.item_names and not explicit_location: # item name hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name) else: # location name hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name) @@ -1036,19 +1031,25 @@ class ClientMessageProcessor(CommonCommandProcessor): hints.append(hint) can_pay -= 1 self.ctx.hints_used[self.client.team, self.client.slot] += 1 + points_available = get_client_points(self.ctx, self.client) if not hint.found: self.ctx.hints[self.client.team, hint.finding_player].add(hint) self.ctx.hints[self.client.team, hint.receiving_player].add(hint) if not_found_hints: - if hints: + if hints and cost and int((points_available // cost) == 0): + self.output( + f"There may be more hintables, however, you cannot afford to pay for any more. " + f" You have {points_available} and need at least " + f"{self.ctx.get_hint_cost(self.client.slot)}.") + elif hints: self.output( "There may be more hintables, you can rerun the command to find more.") else: self.output(f"You can't afford the hint. " f"You have {points_available} points and need at least " - f"{self.ctx.get_hint_cost(self.client.slot)}") + f"{self.ctx.get_hint_cost(self.client.slot)}.") notify_hints(self.ctx, self.client.team, hints) self.ctx.save() return True @@ -1060,6 +1061,22 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output(response) return False + @mark_raw + def _cmd_hint(self, item_or_location: str = "") -> bool: + """Use !hint {item_name/location_name}, + for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or 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_or_location) + + @mark_raw + def _cmd_hint_location(self, location: str = "") -> bool: + """Use !hint_location {location_name}, + for example !hint atomic-bomb to get a spoiler peek for that location. + (In the case of factorio, or any other game where item names and location names are identical, + this command must be used explicitly.)""" + return self.get_hints(location, True) + def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]: return [location_id for From 438e53d25e2cf16e67ab9e651fca73a443282c78 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 9 Oct 2021 20:48:13 -0700 Subject: [PATCH 11/27] hints for visible tech should be free no matter who it is for. --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index 776438ac..638fecb2 100644 --- a/Main.py +++ b/Main.py @@ -270,7 +270,7 @@ def main(args, seed=None): # item code None should be event, location.address should then also be None assert location.item.code is not None locations_data[location.player][location.address] = location.item.code, location.item.player - if location.player in sending_visible_players and location.item.player != location.player: + if location.player in sending_visible_players: precollect_hint(location) elif location.name in world.start_location_hints[location.player]: precollect_hint(location) From 96ffe95404b9bcc3b49f21d41930c05055805d34 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 9 Oct 2021 21:03:03 -0700 Subject: [PATCH 12/27] hopefully fix lint error --- worlds/factorio/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index e9c779a7..fa446759 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -9,7 +9,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies from .Shapes import get_shapes from .Mod import generate_mod -from .Options import factorio_options, Silo +from .Options import factorio_options, Silo, TechTreeInformation import logging @@ -66,7 +66,7 @@ class Factorio(World): if map_basic_settings.get("seed", None) is None: # allow seed 0 map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint - self.sending_visible = self.world.tech_tree_information[player] == Options.TechTreeInformation.option_full + self.sending_visible = self.world.tech_tree_information[player] == TechTreeInformation.option_full generate_output = generate_mod From e66a2a7c30a2e9d42d2d751be8d159acb04fb6d1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Oct 2021 16:50:01 +0200 Subject: [PATCH 13/27] Core: change precollected_items to dict-style Core: make sure there are enough threads available during generate_output to prevent deadlocks if event waiting is used --- BaseClasses.py | 10 ++++++---- Main.py | 19 +++++++++---------- test/inverted_owg/TestInvertedOWG.py | 2 +- test/owg/TestVanillaOWG.py | 2 +- worlds/alttp/Rom.py | 4 +--- worlds/oot/__init__.py | 16 +++++++++------- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 423ffbd5..e818f629 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -27,6 +27,7 @@ class MultiWorld(): plando_connections: List worlds: Dict[int, Any] is_race: bool = False + precollected_items: Dict[int, List[Item]] class AttributeProxy(): def __init__(self, rule): @@ -46,7 +47,7 @@ class MultiWorld(): self.itempool = [] self.seed = None self.seed_name: str = "Unavailable" - self.precollected_items = [] + self.precollected_items = {player: [] for player in self.player_ids} self.state = CollectionState(self) self._cached_entrances = None self._cached_locations = None @@ -266,7 +267,7 @@ class MultiWorld(): def push_precollected(self, item: Item): item.world = self - self.precollected_items.append(item) + self.precollected_items[item.player].append(item) self.state.collect(item, True) def push_item(self, location: Location, item: Item, collect: bool = True): @@ -473,8 +474,9 @@ class CollectionState(object): self.path = {} self.locations_checked = set() self.stale = {player: True for player in range(1, parent.players + 1)} - for item in parent.precollected_items: - self.collect(item, True) + for items in parent.precollected_items.values(): + for item in items: + self.collect(item, True) def update_reachable_regions(self, player: int): from worlds.alttp.EntranceShuffle import indirect_connections diff --git a/Main.py b/Main.py index 638fecb2..a4a718a8 100644 --- a/Main.py +++ b/Main.py @@ -1,4 +1,4 @@ -from itertools import zip_longest +from itertools import zip_longest, chain import logging import os import time @@ -159,16 +159,15 @@ def main(args, seed=None): output = tempfile.TemporaryDirectory() with output as temp_dir: - with concurrent.futures.ThreadPoolExecutor() as pool: + with concurrent.futures.ThreadPoolExecutor(world.players+2) as pool: check_accessibility_task = pool.submit(world.fulfills_accessibility) - output_file_futures = [] - + output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)] for player in world.player_ids: # skip starting a thread for methods that say "pass". if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__: output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) - output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)) + def get_entrance_to_region(region: Region): for entrance in region.entrances: @@ -246,9 +245,8 @@ def main(args, seed=None): for slot in world.player_ids: client_versions[slot] = world.worlds[slot].get_required_client_version() games[slot] = world.game[slot] - precollected_items = {player: [] for player in range(1, world.players + 1)} - for item in world.precollected_items: - precollected_items[item.player].append(item.code) + precollected_items = {player: [item.code for item in world_precollected] + for player, world_precollected in world.precollected_items.items()} precollected_hints = {player: set() for player in range(1, world.players + 1)} # for now special case Factorio tech_tree_information sending_visible_players = set() @@ -397,7 +395,7 @@ def create_playthrough(world): # second phase, sphere 0 removed_precollected = [] - for item in (i for i in world.precollected_items if i.advancement): + for item in (i for i in chain(world.precollected_items.values()) if i.advancement): logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) world.precollected_items.remove(item) world.state.remove(item) @@ -463,7 +461,8 @@ def create_playthrough(world): get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])} + world.spoiler.playthrough = {"0": sorted([str(item) for item in chain(world.precollected_items.values()) + if item.advancement])} for i, sphere in enumerate(collection_spheres): world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)} diff --git a/test/inverted_owg/TestInvertedOWG.py b/test/inverted_owg/TestInvertedOWG.py index 486c3cb9..7192fcb0 100644 --- a/test/inverted_owg/TestInvertedOWG.py +++ b/test/inverted_owg/TestInvertedOWG.py @@ -35,7 +35,7 @@ class TestInvertedOWG(TestBase): self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1)) self.world.get_location('Agahnim 1', 1).item = None self.world.get_location('Agahnim 2', 1).item = None - self.world.precollected_items.clear() + self.world.precollected_items[1].clear() self.world.itempool.append(ItemFactory('Pegasus Boots', 1)) mark_light_world_regions(self.world, 1) self.world.worlds[1].set_rules() diff --git a/test/owg/TestVanillaOWG.py b/test/owg/TestVanillaOWG.py index a4e584f8..68b10732 100644 --- a/test/owg/TestVanillaOWG.py +++ b/test/owg/TestVanillaOWG.py @@ -34,7 +34,7 @@ class TestVanillaOWG(TestBase): self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1)) self.world.get_location('Agahnim 1', 1).item = None self.world.get_location('Agahnim 2', 1).item = None - self.world.precollected_items.clear() + self.world.precollected_items[1].clear() self.world.itempool.append(ItemFactory('Pegasus Boots', 1)) mark_dark_world_regions(self.world, 1) self.world.worlds[1].set_rules() \ No newline at end of file diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index c3367267..568ef893 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1315,9 +1315,7 @@ def patch_rom(world, rom, player, enemized): equip[0x37B] = 1 equip[0x36E] = 0x80 - for item in world.precollected_items: - if item.player != player: - continue + for item in world.precollected_items[player]: if item.name in {'Bow', 'Silver Bow', 'Silver Arrows', 'Progressive Bow', 'Progressive Bow (Alt)', 'Titans Mitts', 'Power Glove', 'Progressive Glove', diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 5e7affb0..e4dc8d84 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -36,7 +36,6 @@ location_id_offset = 67000 # OoT's generate_output doesn't benefit from more than 2 threads, instead it uses a lot of memory. i_o_limiter = threading.Semaphore(2) -hint_data_available = threading.Event() class OOTWorld(World): @@ -88,6 +87,10 @@ class OOTWorld(World): return super().__new__(cls) + def __init__(self, world, player): + self.hint_data_available = threading.Event() + super(OOTWorld, self).__init__(world, player) + def generate_early(self): # Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16: @@ -261,7 +264,7 @@ class OOTWorld(World): # Both two-handed swords can be required in glitch logic, so only consider them nonprogression in glitchless self.nonadvancement_items.add('Biggoron Sword') self.nonadvancement_items.add('Giants Knife') - + def load_regions_from_json(self, file_path): region_json = read_json(file_path) @@ -456,9 +459,7 @@ class OOTWorld(World): junk_pool = get_junk_pool(self) removed_items = [] # Determine starting items - for item in self.world.precollected_items: - if item.player != self.player: - continue + for item in self.world.precollected_items[self.player]: if item.name in self.remove_from_start_inventory: self.remove_from_start_inventory.remove(item.name) removed_items.append(item.name) @@ -703,7 +704,7 @@ class OOTWorld(World): def generate_output(self, output_directory: str): if self.hints != 'none': - hint_data_available.wait() + self.hint_data_available.wait() with i_o_limiter: # Make ice traps appear as other random items @@ -782,7 +783,8 @@ class OOTWorld(World): except Exception as e: raise e finally: - hint_data_available.set() + for autoworld in world.get_game_worlds("Ocarina of Time"): + autoworld.hint_data_available.set() def modify_multidata(self, multidata: dict): for item_name in self.remove_from_start_inventory: From 8f66f94ffab3cdc0913a00c79df661133836f1b7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Oct 2021 20:14:11 +0200 Subject: [PATCH 14/27] WebHost: Generate: Fix dead link --- WebHostLib/templates/generate.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 13720313..398463a6 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -14,8 +14,9 @@

Upload Config{% if race %} (Race Mode){% endif %}

This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files. - If you do not have a config (yaml) file yet, you may create one on the - Player Settings page. + If you do not have a config (yaml) file yet, you may create one on the game's settings page, + which you can find via the + Game List.

{% if race -%} From 952d878442112026181a832fb3216dd1d1c81bfd Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 10 Oct 2021 13:05:41 +0200 Subject: [PATCH 15/27] Marked items as never exclude + some more refactorings --- worlds/timespinner/Items.py | 105 +++++++++++++++++---------------- worlds/timespinner/__init__.py | 50 ++++++++-------- 2 files changed, 78 insertions(+), 77 deletions(-) diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index 8641cb9a..cfa1bc45 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -5,10 +5,11 @@ class ItemData(NamedTuple): code: int count: int = 1 progression: bool = False + never_exclude: bool = False # A lot of items arent normally dropped by the randomizer as they are mostly enemy drops, but they can be enabled if desired item_table: Dict[str, ItemData] = { - 'Eternal Crown': ItemData('Equipment', 1337000), + 'Eternal Crown': ItemData('Equipment', 1337000, never_exclude=True), 'Security Visor': ItemData('Equipment', 1337001, 0), 'Engineer Goggles': ItemData('Equipment', 1337002, 0), 'Leather Helmet': ItemData('Equipment', 1337003, 0), @@ -21,8 +22,8 @@ item_table: Dict[str, ItemData] = { 'Combat Helmet': ItemData('Equipment', 1337010, 0), 'Captain\'s Cap': ItemData('Equipment', 1337011), 'Lab Glasses': ItemData('Equipment', 1337012), - 'Empire Crown': ItemData('Equipment', 1337013), - 'Viletian Crown': ItemData('Equipment', 1337014), + 'Empire Crown': ItemData('Equipment', 1337013, never_exclude=True), + 'Viletian Crown': ItemData('Equipment', 1337014, never_exclude=True), 'Sunglasses': ItemData('Equipment', 1337015, 0), 'Old Coat': ItemData('Equipment', 1337016), 'Trendy Jacket': ItemData('Equipment', 1337017, 0), @@ -37,26 +38,26 @@ item_table: Dict[str, ItemData] = { 'Military Armor': ItemData('Equipment', 1337026, 0), 'Captain\'s Uniform': ItemData('Equipment', 1337027), 'Lab Coat': ItemData('Equipment', 1337028), - 'Empress Robe': ItemData('Equipment', 1337029), - 'Princess Dress': ItemData('Equipment', 1337030), - 'Eternal Coat': ItemData('Equipment', 1337031), + 'Empress Robe': ItemData('Equipment', 1337029, never_exclude=True), + 'Princess Dress': ItemData('Equipment', 1337030, never_exclude=True), + 'Eternal Coat': ItemData('Equipment', 1337031, never_exclude=True), 'Synthetic Plume': ItemData('Equipment', 1337032, 0), 'Cheveur Plume': ItemData('Equipment', 1337033, 0), 'Metal Wristband': ItemData('Equipment', 1337034), 'Nymph Hairband': ItemData('Equipment', 1337035, 0), 'Mother o\' Pearl': ItemData('Equipment', 1337036, 0), - 'Bird Statue': ItemData('Equipment', 1337037), + 'Bird Statue': ItemData('Equipment', 1337037, never_exclude=True), 'Chaos Stole': ItemData('Equipment', 1337038, 0), - 'Pendulum': ItemData('Equipment', 1337039), + 'Pendulum': ItemData('Equipment', 1337039, never_exclude=True), 'Chaos Horn': ItemData('Equipment', 1337040, 0), - 'Filigree Clasp': ItemData('Equipment', 1337041), + 'Filigree Clasp': ItemData('Equipment', 1337041, never_exclude=True), 'Azure Stole': ItemData('Equipment', 1337042, 0), - 'Ancient Coin': ItemData('Equipment', 1337043), + 'Ancient Coin': ItemData('Equipment', 1337043, never_exclude=True), 'Shiny Rock': ItemData('Equipment', 1337044, 0), - 'Galaxy Earrings': ItemData('Equipment', 1337045), - 'Selen\'s Bangle': ItemData('Equipment', 1337046), - 'Glass Pumpkin': ItemData('Equipment', 1337047), - 'Gilded Egg': ItemData('Equipment', 1337048), + 'Galaxy Earrings': ItemData('Equipment', 1337045, never_exclude=True), + 'Selen\'s Bangle': ItemData('Equipment', 1337046, never_exclude=True), + 'Glass Pumpkin': ItemData('Equipment', 1337047, never_exclude=True), + 'Gilded Egg': ItemData('Equipment', 1337048, never_exclude=True), 'Meyef': ItemData('Familiar', 1337049), 'Griffin': ItemData('Familiar', 1337050), 'Merchant Crow': ItemData('Familiar', 1337051), @@ -134,58 +135,58 @@ item_table: Dict[str, ItemData] = { 'Library Keycard V': ItemData('Relic', 1337123, progression=True), 'Tablet': ItemData('Relic', 1337124, progression=True), 'Elevator Keycard': ItemData('Relic', 1337125, progression=True), - 'Jewelry Box': ItemData('Relic', 1337126), + 'Jewelry Box': ItemData('Relic', 1337126, never_exclude=True), 'Goddess Brooch': ItemData('Relic', 1337127), 'Wyrm Brooch': ItemData('Relic', 1337128), 'Greed Brooch': ItemData('Relic', 1337129), 'Eternal Brooch': ItemData('Relic', 1337130), - 'Blue Orb': ItemData('Orb Melee', 1337131), - 'Blade Orb': ItemData('Orb Melee', 1337132), - 'Fire Orb': ItemData('Orb Melee', 1337133, progression=True), - 'Plasma Orb': ItemData('Orb Melee', 1337134, progression=True), - 'Iron Orb': ItemData('Orb Melee', 1337135), - 'Ice Orb': ItemData('Orb Melee', 1337136), - 'Wind Orb': ItemData('Orb Melee', 1337137), - 'Gun Orb': ItemData('Orb Melee', 1337138), - 'Umbra Orb': ItemData('Orb Melee', 1337139), - 'Empire Orb': ItemData('Orb Melee', 1337140), - 'Eye Orb': ItemData('Orb Melee', 1337141), - 'Blood Orb': ItemData('Orb Melee', 1337142), - 'Forbidden Tome': ItemData('Orb Melee', 1337143), - 'Shattered Orb': ItemData('Orb Melee', 1337144), - 'Nether Orb': ItemData('Orb Melee', 1337145), - 'Radiant Orb': ItemData('Orb Melee', 1337146), - 'Aura Blast': ItemData('Orb Spell', 1337147), - 'Colossal Blade': ItemData('Orb Spell', 1337148), - 'Infernal Flames': ItemData('Orb Spell', 1337149, progression=True), - 'Plasma Geyser': ItemData('Orb Spell', 1337150, progression=True), - 'Colossal Hammer': ItemData('Orb Spell', 1337151), - 'Frozen Spires': ItemData('Orb Spell', 1337152), - 'Storm Eye': ItemData('Orb Spell', 1337153), - 'Arm Cannon': ItemData('Orb Spell', 1337154), - 'Dark Flames': ItemData('Orb Spell', 1337155), - 'Aura Serpent': ItemData('Orb Spell', 1337156), - 'Chaos Blades': ItemData('Orb Spell', 1337157), - 'Crimson Vortex': ItemData('Orb Spell', 1337158), - 'Djinn Inferno': ItemData('Orb Spell', 1337159, progression=True), - 'Bombardment': ItemData('Orb Spell', 1337160), - 'Corruption': ItemData('Orb Spell', 1337161), - 'Lightwall': ItemData('Orb Spell', 1337162, progression=True), - 'Bleak Ring': ItemData('Orb Passive', 1337163), + 'Blue Orb': ItemData('Orb Melee', 1337131, never_exclude=True), + 'Blade Orb': ItemData('Orb Melee', 1337132, never_exclude=True), + 'Fire Orb': ItemData('Orb Melee', 1337133, never_exclude=True, progression=True), + 'Plasma Orb': ItemData('Orb Melee', 1337134, never_exclude=True, progression=True), + 'Iron Orb': ItemData('Orb Melee', 1337135, never_exclude=True), + 'Ice Orb': ItemData('Orb Melee', 1337136, never_exclude=True), + 'Wind Orb': ItemData('Orb Melee', 1337137, never_exclude=True), + 'Gun Orb': ItemData('Orb Melee', 1337138, never_exclude=True), + 'Umbra Orb': ItemData('Orb Melee', 1337139, never_exclude=True), + 'Empire Orb': ItemData('Orb Melee', 1337140, never_exclude=True), + 'Eye Orb': ItemData('Orb Melee', 1337141, never_exclude=True), + 'Blood Orb': ItemData('Orb Melee', 1337142, never_exclude=True), + 'Forbidden Tome': ItemData('Orb Melee', 1337143, never_exclude=True), + 'Shattered Orb': ItemData('Orb Melee', 1337144, never_exclude=True), + 'Nether Orb': ItemData('Orb Melee', 1337145, never_exclude=True), + 'Radiant Orb': ItemData('Orb Melee', 1337146, never_exclude=True), + 'Aura Blast': ItemData('Orb Spell', 1337147, never_exclude=True), + 'Colossal Blade': ItemData('Orb Spell', 1337148, never_exclude=True), + 'Infernal Flames': ItemData('Orb Spell', 1337149, never_exclude=True, progression=True), + 'Plasma Geyser': ItemData('Orb Spell', 1337150, never_exclude=True, progression=True), + 'Colossal Hammer': ItemData('Orb Spell', 1337151, never_exclude=True), + 'Frozen Spires': ItemData('Orb Spell', 1337152, never_exclude=True), + 'Storm Eye': ItemData('Orb Spell', 1337153, never_exclude=True), + 'Arm Cannon': ItemData('Orb Spell', 1337154, never_exclude=True), + 'Dark Flames': ItemData('Orb Spell', 1337155, never_exclude=True), + 'Aura Serpent': ItemData('Orb Spell', 1337156, never_exclude=True), + 'Chaos Blades': ItemData('Orb Spell', 1337157, never_exclude=True), + 'Crimson Vortex': ItemData('Orb Spell', 1337158, never_exclude=True), + 'Djinn Inferno': ItemData('Orb Spell', 1337159, never_exclude=True, progression=True), + 'Bombardment': ItemData('Orb Spell', 1337160, never_exclude=True), + 'Corruption': ItemData('Orb Spell', 1337161, never_exclude=True), + 'Lightwall': ItemData('Orb Spell', 1337162, never_exclude=True, progression=True), + 'Bleak Ring': ItemData('Orb Passive', 1337163, never_exclude=True), 'Scythe Ring': ItemData('Orb Passive', 1337164), - 'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True), - 'Royal Ring': ItemData('Orb Passive', 1337166, progression=True), + 'Pyro Ring': ItemData('Orb Passive', 1337165, never_exclude=True, progression=True), + 'Royal Ring': ItemData('Orb Passive', 1337166, never_exclude=True, progression=True), 'Shield Ring': ItemData('Orb Passive', 1337167), 'Icicle Ring': ItemData('Orb Passive', 1337168), 'Tailwind Ring': ItemData('Orb Passive', 1337169), 'Economizer Ring': ItemData('Orb Passive', 1337170), 'Dusk Ring': ItemData('Orb Passive', 1337171), - 'Star of Lachiem': ItemData('Orb Passive', 1337172), + 'Star of Lachiem': ItemData('Orb Passive', 1337172, never_exclude=True), 'Oculus Ring': ItemData('Orb Passive', 1337173, progression=True), 'Sanguine Ring': ItemData('Orb Passive', 1337174), 'Sun Ring': ItemData('Orb Passive', 1337175), 'Silence Ring': ItemData('Orb Passive', 1337176), - 'Shadow Seal': ItemData('Orb Passive', 1337177), + 'Shadow Seal': ItemData('Orb Passive', 1337177, never_exclude=True), 'Hope Ring': ItemData('Orb Passive', 1337178), 'Max HP': ItemData('Stat', 1337179, 12), 'Max Aura': ItemData('Stat', 1337180, 13), diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index e58d0d92..468e2b78 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -40,11 +40,11 @@ class TimespinnerWorld(World): def create_item(self, name: str) -> Item: - return create_item(name, self.player) + return create_item_with_correct_settings(self.world, self.player, name) def set_rules(self): - setup_events(self.world, self.player, self.locked_locations[self.player]) + setup_events(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player]) self.world.completion_condition[self.player] = lambda state: state.has('Killed Nightmare', self.player) @@ -59,7 +59,7 @@ class TimespinnerWorld(World): pool = get_item_pool(self.world, self.player, excluded_items) - fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], pool) + fill_item_pool_with_dummy_items(self.world, self.player, self.locked_locations[self.player], self.location_cache[self.player], pool) self.world.itempool += pool @@ -79,11 +79,6 @@ class TimespinnerWorld(World): return slot_data -def create_item(name: str, player: int) -> Item: - data = item_table[name] - return Item(name, data.progression, data.code, player) - - def get_excluded_items_based_on_options(world: MultiWorld, player: int) -> Set[str]: excluded_items: Set[str] = set() @@ -104,8 +99,8 @@ def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str excluded_items.add(melee_weapon) excluded_items.add(spell) - melee_weapon_item = create_item(melee_weapon, player) - spell_item = create_item(spell, player) + melee_weapon_item = create_item_with_correct_settings(world, player, melee_weapon) + spell_item = create_item_with_correct_settings(world, player, spell) world.get_location('Yo Momma 1', player).place_locked_item(melee_weapon_item) world.get_location('Yo Momma 2', player).place_locked_item(spell_item) @@ -120,15 +115,16 @@ def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> L for name, data in item_table.items(): if not name in excluded_items: for _ in range(data.count): - item = update_progressive_state_based_flags(world, player, name, create_item(name, player)) + item = create_item_with_correct_settings(world, player, name) pool.append(item) return pool -def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str], pool: List[Item]): - for _ in range(len(get_locations(world, player)) - len(locked_locations) - len(pool)): - item = create_item(world.random.choice(filler_items), player) +def fill_item_pool_with_dummy_items(world: MultiWorld, player: int, locked_locations: List[str], + location_cache: List[Location], pool: List[Item]): + for _ in range(len(location_cache) - len(locked_locations) - len(pool)): + item = create_item_with_correct_settings(world, player, world.random.choice(filler_items)) pool.append(item) @@ -139,27 +135,31 @@ def place_first_progression_item(world: MultiWorld, player: int, excluded_items: excluded_items.add(progression_item) locked_locations.append(location) - item = create_item(progression_item, player) + item = create_item_with_correct_settings(world, player, progression_item) world.get_location(location, player).place_locked_item(item) -def update_progressive_state_based_flags(world: MultiWorld, player: int, name: str, data: Item) -> Item: - if not data.advancement: - return data +def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) -> Item: + data = item_table[name] + + item = Item(name, data.progression, data.code, player) + item.never_exclude = data.never_exclude + + if not item.advancement: + return item if (name == 'Tablet' or name == 'Library Keycard V') and not is_option_enabled(world, player, "DownloadableItems"): - data.advancement = False + item.advancement = False if name == 'Oculus Ring' and not is_option_enabled(world, player, "FacebookMode"): - data.advancement = False + item.advancement = False - return data + return item -def setup_events(world: MultiWorld, player: int, locked_locations: List[str]): - for location in get_locations(world, player): - if location.code == EventId: - location = world.get_location(location.name, player) +def setup_events(world: MultiWorld, player: int, locked_locations: List[str], location_cache: List[Location]): + for location in location_cache: + if location.address == EventId: item = Item(location.name, True, EventId, player) locked_locations.append(location.name) From e301b67e49e8e14ebf36f246c85c73ee99c19f8c Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 10 Oct 2021 14:23:06 +0200 Subject: [PATCH 16/27] Greatly improved performance when no locations are excluded --- worlds/timespinner/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 468e2b78..a475aba6 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -144,7 +144,9 @@ def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) data = item_table[name] item = Item(name, data.progression, data.code, player) - item.never_exclude = data.never_exclude + + if world.exclude_locations[player]: # Doubles performance to not set item exclusion when its not required + item.never_exclude = data.never_exclude if not item.advancement: return item From 3e6f7f0fad5b1c98c314f03cf50a918f8b2d173b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 10 Oct 2021 21:52:45 +0200 Subject: [PATCH 17/27] WebHost: add /discord redirect --- WebHostLib/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 2c749594..b42f55d9 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -180,6 +180,9 @@ 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") from WebHostLib.customserver import run_server_process from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it From f7bd637073a6e2428c78153f3e12799b81d9c045 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 11 Oct 2021 00:12:00 +0200 Subject: [PATCH 18/27] Core: fix chain != chain.from_iterable --- Main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Main.py b/Main.py index a4a718a8..3c2fb39b 100644 --- a/Main.py +++ b/Main.py @@ -395,7 +395,7 @@ def create_playthrough(world): # second phase, sphere 0 removed_precollected = [] - for item in (i for i in chain(world.precollected_items.values()) if i.advancement): + for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement): logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) world.precollected_items.remove(item) world.state.remove(item) @@ -461,7 +461,8 @@ def create_playthrough(world): get_path(state, world.get_region('Inverted Big Bomb Shop', player)) # we can finally output our playthrough - world.spoiler.playthrough = {"0": sorted([str(item) for item in chain(world.precollected_items.values()) + world.spoiler.playthrough = {"0": sorted([str(item) for item in + chain.from_iterable(world.precollected_items.values()) if item.advancement])} for i, sphere in enumerate(collection_spheres): From a8b105267cb64ae79be73c7f566138750d29637a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 11 Oct 2021 00:46:18 +0200 Subject: [PATCH 19/27] WebHost: add hint cost and forfeit mode to webgen page --- Main.py | 30 +++++++++++++++++---------- WebHostLib/autolauncher.py | 2 +- WebHostLib/generate.py | 28 +++++++++++++++++++------ WebHostLib/static/styles/generate.css | 2 +- WebHostLib/templates/generate.html | 12 +++++++++++ WebHostLib/templates/viewSeed.html | 16 -------------- 6 files changed, 55 insertions(+), 35 deletions(-) diff --git a/Main.py b/Main.py index 3c2fb39b..abf3a380 100644 --- a/Main.py +++ b/Main.py @@ -7,7 +7,7 @@ import concurrent.futures import pickle import tempfile import zipfile -from typing import Dict, Tuple +from typing import Dict, Tuple, Optional from BaseClasses import MultiWorld, CollectionState, Region, RegionType from worlds.alttp.Items import item_name_groups @@ -19,7 +19,16 @@ from worlds.generic.Rules import locality_rules, exclusion_rules from worlds import AutoWorld -def main(args, seed=None): +ordered_areas = ( + 'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', + 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', + 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total" +) + + +def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None): + if not baked_server_options: + baked_server_options = get_options()["server_options"] if args.outputpath: os.makedirs(args.outputpath, exist_ok=True) output_path.cached_path = args.outputpath @@ -159,15 +168,15 @@ def main(args, seed=None): output = tempfile.TemporaryDirectory() with output as temp_dir: - with concurrent.futures.ThreadPoolExecutor(world.players+2) as pool: + with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool: check_accessibility_task = pool.submit(world.fulfills_accessibility) output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)] for player in world.player_ids: # skip starting a thread for methods that say "pass". if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__: - output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) - + output_file_futures.append( + pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) def get_entrance_to_region(region: Region): for entrance in region.entrances: @@ -188,9 +197,7 @@ def main(args, seed=None): if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: er_hint_data[region.player][location.address] = main_entrance.name - ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', - 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', - 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total") + checks_in_area = {player: {area: list() for area in ordered_areas} for player in range(1, world.players + 1)} @@ -219,8 +226,9 @@ def main(args, seed=None): 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[player]]: - item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], - region.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 @@ -286,7 +294,7 @@ def main(args, seed=None): world.worlds[player].remote_start_inventory}, "locations": locations_data, "checks_in_area": checks_in_area, - "server_options": get_options()["server_options"], + "server_options": baked_server_options, "er_hint_data": er_hint_data, "precollected_items": precollected_items, "precollected_hints": precollected_hints, diff --git a/WebHostLib/autolauncher.py b/WebHostLib/autolauncher.py index b2f60812..0c1c2b6d 100644 --- a/WebHostLib/autolauncher.py +++ b/WebHostLib/autolauncher.py @@ -89,7 +89,7 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation): options = restricted_loads(generation.options) logging.info(f"Generating {generation.id} for {len(options)} players") pool.apply_async(gen_game, (options,), - {"race": meta["race"], + {"meta": meta, "sid": generation.id, "owner": generation.owner}, handle_generation_success, handle_generation_failure) diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index 60251ca3..ac16d7c9 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -4,6 +4,7 @@ import random import json import zipfile from collections import Counter +from typing import Dict, Optional as TypeOptional from flask import request, flash, redirect, url_for, session, render_template @@ -33,6 +34,14 @@ def generate(race=False): flash(options) else: results, gen_options = roll_options(options) + # get form data -> server settings + hint_cost = int(request.form.get("hint_cost", 10)) + forfeit_mode = request.form.get("forfeit_mode", "goal") + meta = {"race": race, "hint_cost": hint_cost, "forfeit_mode": forfeit_mode} + if race: + meta["item_cheat"] = False + meta["remaining"] = False + if any(type(result) == str for result in results.values()): return render_template("checkResult.html", results=results) elif len(gen_options) > app.config["MAX_ROLL"]: @@ -42,7 +51,8 @@ def generate(race=False): gen = Generation( options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), # convert to json compatible - meta=json.dumps({"race": race}), state=STATE_QUEUED, + meta=json.dumps(meta), + state=STATE_QUEUED, owner=session["_id"]) commit() @@ -50,18 +60,24 @@ def generate(race=False): else: try: seed_id = gen_game({name: vars(options) for name, options in gen_options.items()}, - race=race, owner=session["_id"].int) + meta=meta, owner=session["_id"].int) except BaseException as e: from .autolauncher import handle_generation_failure handle_generation_failure(e) - return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e))) + return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e))) return redirect(url_for("viewSeed", seed=seed_id)) return render_template("generate.html", race=race) -def gen_game(gen_options, race=False, owner=None, sid=None): +def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None): + if not meta: + meta: Dict[str, object] = {} + + meta.setdefault("hint_cost", 10) + race = meta.get("race", False) + del (meta["race"]) try: target = tempfile.TemporaryDirectory() playercount = len(gen_options) @@ -95,7 +111,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None): erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0] erargs.name[player] = handle_name(erargs.name[player], player, name_counter) - ERmain(erargs, seed) + ERmain(erargs, seed, baked_server_options=meta) return upload_to_db(target.name, sid, owner, race) except BaseException as e: @@ -105,7 +121,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None): if gen is not None: gen.state = STATE_ERROR meta = json.loads(gen.meta) - meta["error"] = (e.__class__.__name__ + ": "+ str(e)) + meta["error"] = (e.__class__.__name__ + ": " + str(e)) gen.meta = json.dumps(meta) commit() diff --git a/WebHostLib/static/styles/generate.css b/WebHostLib/static/styles/generate.css index d7406682..676865c0 100644 --- a/WebHostLib/static/styles/generate.css +++ b/WebHostLib/static/styles/generate.css @@ -25,6 +25,6 @@ margin-bottom: 1rem; } -#generate-game-form{ +#file-input{ display: none; } diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 398463a6..57bc25f8 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -37,6 +37,18 @@

+ +
diff --git a/WebHostLib/templates/viewSeed.html b/WebHostLib/templates/viewSeed.html index d8ba3d1c..36271cad 100644 --- a/WebHostLib/templates/viewSeed.html +++ b/WebHostLib/templates/viewSeed.html @@ -12,10 +12,6 @@

Seed Info

- {% if not seed.multidata and not seed.spoiler %} -

Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms - cannot be created.

- {% endif %} @@ -33,18 +29,6 @@ {% endif %} {% if seed.multidata %} - - - -
Players:  - -
Rooms:  From 78443bffaceb0863b1fca5a2ae30317bd555df56 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 11 Oct 2021 01:39:25 +0200 Subject: [PATCH 20/27] Core: fix missed precollected change --- Main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Main.py b/Main.py index abf3a380..f8a74e79 100644 --- a/Main.py +++ b/Main.py @@ -405,7 +405,7 @@ def create_playthrough(world): removed_precollected = [] for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement): logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player) - world.precollected_items.remove(item) + world.precollected_items[item.player].remove(item) world.state.remove(item) if not world.can_beat_game(): world.push_precollected(item) From 065931cae77ceeceed099b14f229bd1ddd19efc7 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Mon, 11 Oct 2021 11:41:45 +0200 Subject: [PATCH 21/27] Greatly reduced number of items marked as never_excluded due to the performance implications it brings --- worlds/timespinner/Items.py | 80 +++++++++++++++++----------------- worlds/timespinner/__init__.py | 4 +- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/worlds/timespinner/Items.py b/worlds/timespinner/Items.py index cfa1bc45..ada37630 100644 --- a/worlds/timespinner/Items.py +++ b/worlds/timespinner/Items.py @@ -22,8 +22,8 @@ item_table: Dict[str, ItemData] = { 'Combat Helmet': ItemData('Equipment', 1337010, 0), 'Captain\'s Cap': ItemData('Equipment', 1337011), 'Lab Glasses': ItemData('Equipment', 1337012), - 'Empire Crown': ItemData('Equipment', 1337013, never_exclude=True), - 'Viletian Crown': ItemData('Equipment', 1337014, never_exclude=True), + 'Empire Crown': ItemData('Equipment', 1337013), + 'Viletian Crown': ItemData('Equipment', 1337014), 'Sunglasses': ItemData('Equipment', 1337015, 0), 'Old Coat': ItemData('Equipment', 1337016), 'Trendy Jacket': ItemData('Equipment', 1337017, 0), @@ -38,8 +38,8 @@ item_table: Dict[str, ItemData] = { 'Military Armor': ItemData('Equipment', 1337026, 0), 'Captain\'s Uniform': ItemData('Equipment', 1337027), 'Lab Coat': ItemData('Equipment', 1337028), - 'Empress Robe': ItemData('Equipment', 1337029, never_exclude=True), - 'Princess Dress': ItemData('Equipment', 1337030, never_exclude=True), + 'Empress Robe': ItemData('Equipment', 1337029), + 'Princess Dress': ItemData('Equipment', 1337030), 'Eternal Coat': ItemData('Equipment', 1337031, never_exclude=True), 'Synthetic Plume': ItemData('Equipment', 1337032, 0), 'Cheveur Plume': ItemData('Equipment', 1337033, 0), @@ -50,9 +50,9 @@ item_table: Dict[str, ItemData] = { 'Chaos Stole': ItemData('Equipment', 1337038, 0), 'Pendulum': ItemData('Equipment', 1337039, never_exclude=True), 'Chaos Horn': ItemData('Equipment', 1337040, 0), - 'Filigree Clasp': ItemData('Equipment', 1337041, never_exclude=True), + 'Filigree Clasp': ItemData('Equipment', 1337041), 'Azure Stole': ItemData('Equipment', 1337042, 0), - 'Ancient Coin': ItemData('Equipment', 1337043, never_exclude=True), + 'Ancient Coin': ItemData('Equipment', 1337043), 'Shiny Rock': ItemData('Equipment', 1337044, 0), 'Galaxy Earrings': ItemData('Equipment', 1337045, never_exclude=True), 'Selen\'s Bangle': ItemData('Equipment', 1337046, never_exclude=True), @@ -140,42 +140,42 @@ item_table: Dict[str, ItemData] = { 'Wyrm Brooch': ItemData('Relic', 1337128), 'Greed Brooch': ItemData('Relic', 1337129), 'Eternal Brooch': ItemData('Relic', 1337130), - 'Blue Orb': ItemData('Orb Melee', 1337131, never_exclude=True), - 'Blade Orb': ItemData('Orb Melee', 1337132, never_exclude=True), - 'Fire Orb': ItemData('Orb Melee', 1337133, never_exclude=True, progression=True), - 'Plasma Orb': ItemData('Orb Melee', 1337134, never_exclude=True, progression=True), - 'Iron Orb': ItemData('Orb Melee', 1337135, never_exclude=True), - 'Ice Orb': ItemData('Orb Melee', 1337136, never_exclude=True), - 'Wind Orb': ItemData('Orb Melee', 1337137, never_exclude=True), - 'Gun Orb': ItemData('Orb Melee', 1337138, never_exclude=True), - 'Umbra Orb': ItemData('Orb Melee', 1337139, never_exclude=True), - 'Empire Orb': ItemData('Orb Melee', 1337140, never_exclude=True), - 'Eye Orb': ItemData('Orb Melee', 1337141, never_exclude=True), - 'Blood Orb': ItemData('Orb Melee', 1337142, never_exclude=True), - 'Forbidden Tome': ItemData('Orb Melee', 1337143, never_exclude=True), - 'Shattered Orb': ItemData('Orb Melee', 1337144, never_exclude=True), - 'Nether Orb': ItemData('Orb Melee', 1337145, never_exclude=True), - 'Radiant Orb': ItemData('Orb Melee', 1337146, never_exclude=True), - 'Aura Blast': ItemData('Orb Spell', 1337147, never_exclude=True), - 'Colossal Blade': ItemData('Orb Spell', 1337148, never_exclude=True), - 'Infernal Flames': ItemData('Orb Spell', 1337149, never_exclude=True, progression=True), - 'Plasma Geyser': ItemData('Orb Spell', 1337150, never_exclude=True, progression=True), - 'Colossal Hammer': ItemData('Orb Spell', 1337151, never_exclude=True), - 'Frozen Spires': ItemData('Orb Spell', 1337152, never_exclude=True), - 'Storm Eye': ItemData('Orb Spell', 1337153, never_exclude=True), - 'Arm Cannon': ItemData('Orb Spell', 1337154, never_exclude=True), - 'Dark Flames': ItemData('Orb Spell', 1337155, never_exclude=True), - 'Aura Serpent': ItemData('Orb Spell', 1337156, never_exclude=True), - 'Chaos Blades': ItemData('Orb Spell', 1337157, never_exclude=True), - 'Crimson Vortex': ItemData('Orb Spell', 1337158, never_exclude=True), - 'Djinn Inferno': ItemData('Orb Spell', 1337159, never_exclude=True, progression=True), - 'Bombardment': ItemData('Orb Spell', 1337160, never_exclude=True), - 'Corruption': ItemData('Orb Spell', 1337161, never_exclude=True), - 'Lightwall': ItemData('Orb Spell', 1337162, never_exclude=True, progression=True), + 'Blue Orb': ItemData('Orb Melee', 1337131), + 'Blade Orb': ItemData('Orb Melee', 1337132), + 'Fire Orb': ItemData('Orb Melee', 1337133, progression=True), + 'Plasma Orb': ItemData('Orb Melee', 1337134, progression=True), + 'Iron Orb': ItemData('Orb Melee', 1337135), + 'Ice Orb': ItemData('Orb Melee', 1337136), + 'Wind Orb': ItemData('Orb Melee', 1337137), + 'Gun Orb': ItemData('Orb Melee', 1337138), + 'Umbra Orb': ItemData('Orb Melee', 1337139), + 'Empire Orb': ItemData('Orb Melee', 1337140), + 'Eye Orb': ItemData('Orb Melee', 1337141), + 'Blood Orb': ItemData('Orb Melee', 1337142), + 'Forbidden Tome': ItemData('Orb Melee', 1337143), + 'Shattered Orb': ItemData('Orb Melee', 1337144), + 'Nether Orb': ItemData('Orb Melee', 1337145), + 'Radiant Orb': ItemData('Orb Melee', 1337146), + 'Aura Blast': ItemData('Orb Spell', 1337147), + 'Colossal Blade': ItemData('Orb Spell', 1337148), + 'Infernal Flames': ItemData('Orb Spell', 1337149, progression=True), + 'Plasma Geyser': ItemData('Orb Spell', 1337150, progression=True), + 'Colossal Hammer': ItemData('Orb Spell', 1337151), + 'Frozen Spires': ItemData('Orb Spell', 1337152), + 'Storm Eye': ItemData('Orb Spell', 1337153), + 'Arm Cannon': ItemData('Orb Spell', 1337154), + 'Dark Flames': ItemData('Orb Spell', 1337155), + 'Aura Serpent': ItemData('Orb Spell', 1337156), + 'Chaos Blades': ItemData('Orb Spell', 1337157), + 'Crimson Vortex': ItemData('Orb Spell', 1337158), + 'Djinn Inferno': ItemData('Orb Spell', 1337159, progression=True), + 'Bombardment': ItemData('Orb Spell', 1337160), + 'Corruption': ItemData('Orb Spell', 1337161), + 'Lightwall': ItemData('Orb Spell', 1337162, progression=True), 'Bleak Ring': ItemData('Orb Passive', 1337163, never_exclude=True), 'Scythe Ring': ItemData('Orb Passive', 1337164), - 'Pyro Ring': ItemData('Orb Passive', 1337165, never_exclude=True, progression=True), - 'Royal Ring': ItemData('Orb Passive', 1337166, never_exclude=True, progression=True), + 'Pyro Ring': ItemData('Orb Passive', 1337165, progression=True), + 'Royal Ring': ItemData('Orb Passive', 1337166, progression=True), 'Shield Ring': ItemData('Orb Passive', 1337167), 'Icicle Ring': ItemData('Orb Passive', 1337168), 'Tailwind Ring': ItemData('Orb Passive', 1337169), diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index a475aba6..468e2b78 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -144,9 +144,7 @@ def create_item_with_correct_settings(world: MultiWorld, player: int, name: str) data = item_table[name] item = Item(name, data.progression, data.code, player) - - if world.exclude_locations[player]: # Doubles performance to not set item exclusion when its not required - item.never_exclude = data.never_exclude + item.never_exclude = data.never_exclude if not item.advancement: return item From e4f4c1f1be6193af8c12cafdbce8716abae26bd7 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 11 Oct 2021 20:52:30 -0400 Subject: [PATCH 22/27] Add Start Playing page, clean up /generate page --- WebHostLib/__init__.py | 6 ++ WebHostLib/static/styles/generate.css | 15 +++++ WebHostLib/static/styles/startPlaying.css | 18 +++++ WebHostLib/templates/generate.html | 74 +++++++++++++-------- WebHostLib/templates/header/baseHeader.html | 2 +- WebHostLib/templates/landing.html | 2 +- WebHostLib/templates/startPlaying.html | 35 ++++++++++ 7 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 WebHostLib/static/styles/startPlaying.css create mode 100644 WebHostLib/templates/startPlaying.html diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index b42f55d9..6463540e 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -82,6 +82,12 @@ 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") + + # Player settings pages @app.route('/games//player-settings') def player_settings(game): diff --git a/WebHostLib/static/styles/generate.css b/WebHostLib/static/styles/generate.css index 676865c0..e0b3f5ec 100644 --- a/WebHostLib/static/styles/generate.css +++ b/WebHostLib/static/styles/generate.css @@ -25,6 +25,21 @@ margin-bottom: 1rem; } +#generate-game-form-wrapper table td{ + text-align: left; + padding-right: 0.5rem; +} + +#generate-form-button-row{ + display: flex; + flex-direction: row; + justify-content: center; +} + #file-input{ display: none; } + +.interactive{ + color: #ffef00; +} diff --git a/WebHostLib/static/styles/startPlaying.css b/WebHostLib/static/styles/startPlaying.css new file mode 100644 index 00000000..c13468fc --- /dev/null +++ b/WebHostLib/static/styles/startPlaying.css @@ -0,0 +1,18 @@ +#start-playing-wrapper{ + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; +} + +#start-playing{ + width: 700px; + min-height: 240px; + text-align: center; +} + +#start-playing-button-row{ + display: flex; + flex-direction: row; + justify-content: space-evenly; +} diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 57bc25f8..a1a18115 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -11,12 +11,11 @@ {% include 'header/oceanHeader.html' %}
-

Upload Config{% if race %} (Race Mode){% endif %}

+

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

- This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files. - If you do not have a config (yaml) file yet, you may create one on the game's settings page, - which you can find via the - Game List. + This page allows you to generate a game by uploading a config file or a zip file containing config + files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page, + which you can find via the supported games list.

{% if race -%} @@ -24,33 +23,54 @@ roms will be encrypted, and single-player games will have no multidata files. {%- else -%} If you would like to generate a race game, - click here. Race games are generated without - a spoiler log, the ROMs are encrypted, and single-player games will not include a multidata file. + click here.
+ Race games are generated without a spoiler log, the ROMs are encrypted, and single-player games + will not include a multidata file. {%- endif -%}

-

- After generation is complete, you will have the option to download a patch file. - This patch file can be opened with the - client, which can be - used to to create a rom file. In-browser patching is planned for the future. -

- - - + + + + + + + + + + + + +
+ +
+ + (?) + + + +
+
+ +
- +
diff --git a/WebHostLib/templates/header/baseHeader.html b/WebHostLib/templates/header/baseHeader.html index dfe42e51..eb8a86a0 100644 --- a/WebHostLib/templates/header/baseHeader.html +++ b/WebHostLib/templates/header/baseHeader.html @@ -13,7 +13,7 @@ diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index ec187dad..28430239 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -13,7 +13,7 @@

multiworld multi-game randomizer

From a94a30168c56d8e3742b12e202e29f1731b5b539 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 11 Oct 2021 21:11:37 -0400 Subject: [PATCH 24/27] Greatly improve the Start Playing page --- WebHostLib/static/styles/startPlaying.css | 6 ------ WebHostLib/templates/startPlaying.html | 21 +++++++++------------ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/WebHostLib/static/styles/startPlaying.css b/WebHostLib/static/styles/startPlaying.css index c13468fc..f04c8af8 100644 --- a/WebHostLib/static/styles/startPlaying.css +++ b/WebHostLib/static/styles/startPlaying.css @@ -10,9 +10,3 @@ min-height: 240px; text-align: center; } - -#start-playing-button-row{ - display: flex; - flex-direction: row; - justify-content: space-evenly; -} diff --git a/WebHostLib/templates/startPlaying.html b/WebHostLib/templates/startPlaying.html index d08f52f6..2502d102 100644 --- a/WebHostLib/templates/startPlaying.html +++ b/WebHostLib/templates/startPlaying.html @@ -13,21 +13,18 @@

Start Playing

- To start playing a game, you'll first need to generate a randomized game. You can do that on this - website, by clicking the button below labeled 'Generate and Host'. + If you're ready to start playing but don't know where to begin, check out the + tutorials page. It has all the resources you need to create a config file + and get started. If you already have a config file, or a zip file containing them, read on.

- If you have already generated a game and would just like to host it on this site, click the button - labeled 'Host Only' + + To start playing a game, you'll first need to generate a randomized game. + You'll need to upload either a config file or a zip file containing one more more config files.

- If you have no idea what any of that means, don't worry! We have a page full of tutorials for each - game that will get you started properly. To get there, click on the button labeled - 'Tutorials'. + + If you have already generated a game and just need to host it, this site can
+ host a pre-generated game for you.

-
From 11fc220d4da7b40db806987480627daf64eaabda Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 11 Oct 2021 21:13:40 -0400 Subject: [PATCH 25/27] Minor wording change on landing page. --- WebHostLib/templates/landing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index 28430239..56d7a1c0 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -50,7 +50,7 @@

{{ seeds }} - games were created and + games were generated and {{ rooms }} were hosted in the last 7 days.

From 79e33899a857d00923ec2add2e32d004e4876901 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 11 Oct 2021 21:20:31 -0400 Subject: [PATCH 26/27] Supported game page game links now point to the game info page. Added a link below for the settings pages. --- WebHostLib/templates/supportedGames.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index af134a00..f6409916 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -10,8 +10,12 @@

Currently Supported Games

{% for game, description in worlds.items() %} -

{{ game }}

-

{{ description }}

+

{{ game }}

+

+ Settings Page +
+ {{ description }} +

{% endfor %}
{% endblock %} From ef8af7d6185b2a30225c249c755238df41244352 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 11 Oct 2021 21:37:08 -0400 Subject: [PATCH 27/27] Move config files and player-settings js files to /generated/configs and /generated/player-settings and update the pages that use them --- WebHostLib/options.py | 10 ++++++++-- WebHostLib/static/assets/player-settings.js | 2 +- WebHostLib/templates/player-settings.html | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index f3c50ae3..9c23242f 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -32,7 +32,10 @@ def create(): dictify_range=dictify_range, default_converter=default_converter, ) - with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f: + if not os.path.isdir(os.path.join(target_folder, 'configs')): + os.mkdir(os.path.join(target_folder, 'configs')) + + with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f: f.write(res) # Generate JSON files for player-settings pages @@ -78,5 +81,8 @@ def create(): player_settings["gameOptions"] = game_options - with open(os.path.join(target_folder, game_name + ".json"), "w") as f: + if not os.path.isdir(os.path.join(target_folder, 'player-settings')): + os.mkdir(os.path.join(target_folder, 'player-settings')) + + with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f: f.write(json.dumps(player_settings, indent=2, separators=(',', ': '))) diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index bd1f2a27..e039e8c0 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -61,7 +61,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => { try{ resolve(JSON.parse(ajax.responseText)); } catch(error){ reject(error); } }; - ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true); + ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true); ajax.send(); }); diff --git a/WebHostLib/templates/player-settings.html b/WebHostLib/templates/player-settings.html index c142d17a..18dc9032 100644 --- a/WebHostLib/templates/player-settings.html +++ b/WebHostLib/templates/player-settings.html @@ -21,7 +21,7 @@ A list of all games you have generated can be found here.
Advanced users can download a template file for this game - here. + here.