From a6a9989fcfef251f989a0ef87f73a6d3b103f121 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 5 Jan 2022 14:15:19 -0500 Subject: [PATCH 01/24] SM small improvements (#190) * added a fallback default starting location instead of failing generation if an invalid one was chosen * added Nothing and NoEnergy as hint blacklist added missing NoEnergy as local items and removed it from progression --- worlds/sm/__init__.py | 4 +++- worlds/sm/variaRandomizer/randomizer.py | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 6abcbfbe..fe8a4870 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -44,6 +44,7 @@ class SMWorld(World): itemManager: ItemManager locations = {} + hint_blacklist = {'Nothing', 'NoEnergy'} Logic.factory('vanilla') @@ -85,6 +86,7 @@ class SMWorld(World): # keeps Nothing items local so no player will ever pickup Nothing # doing so reduces contribution of this world to the Multiworld the more Nothing there is though self.world.local_items[self.player].value.add('Nothing') + self.world.local_items[self.player].value.add('NoEnergy') if (self.variaRando.args.morphPlacement == "early"): self.world.local_items[self.player].value.add('Morph') @@ -126,7 +128,7 @@ class SMWorld(World): weaponCount[2] += 1 else: isAdvancement = False - elif item.Type == 'Nothing': + elif item.Category == 'Nothing': isAdvancement = False itemClass = ItemManager.Items[item.Type].Class diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index a95764df..0da2b2d0 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -16,6 +16,7 @@ from utils.doorsmanager import DoorsManager from logic.logic import Logic import utils.log +from worlds.sm.Options import StartLocation # we need to know the logic before doing anything else def getLogic(): @@ -498,10 +499,12 @@ class VariaRandomizer: sys.exit(-1) args.startLocation = random.choice(possibleStartAPs) elif args.startLocation not in possibleStartAPs: - optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) - optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) - dumpErrorMsgs(args.output, optErrMsgs) - sys.exit(-1) + args.startLocation = 'Landing Site' + world.start_location[player] = StartLocation(StartLocation.default) + #optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) + #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) + #dumpErrorMsgs(args.output, optErrMsgs) + #sys.exit(-1) ap = getAccessPoint(args.startLocation) if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True: forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location") From 80b3a5b1d49141d1cbb7953cb5e2feab5c4a4bf6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 6 Jan 2022 06:09:15 +0100 Subject: [PATCH 02/24] WebHost: fix is_zipfile check for flask FileStorage objects - and assorted cleanup --- BaseClasses.py | 5 ++--- WebHostLib/upload.py | 2 +- test/TestBase.py | 1 + test/base/__init__.py | 0 test/{base => general}/TestFill.py | 10 +++++----- test/minor_glitches/TestMinor.py | 10 ++++++---- worlds/hk/__init__.py | 4 ---- 7 files changed, 15 insertions(+), 17 deletions(-) delete mode 100644 test/base/__init__.py rename test/{base => general}/TestFill.py (98%) diff --git a/BaseClasses.py b/BaseClasses.py index bbf110d2..13e74d13 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -782,10 +782,9 @@ class RegionType(int, Enum): class Region(object): - - def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None): + def __init__(self, name: str, type_: RegionType, hint, player: int, world: Optional[MultiWorld] = None): self.name = name - self.type = type + self.type = type_ self.entrances = [] self.exits = [] self.locations = [] diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index cdd7e315..607b0aff 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -100,7 +100,7 @@ def uploads(): if file.filename == '': flash('No selected file') elif file and allowed_file(file.filename): - if zipfile.is_zipfile(file.filename): + if zipfile.is_zipfile(file): with zipfile.ZipFile(file, 'r') as zfile: res = upload_zip_to_db(zfile) if type(res) == str: diff --git a/test/TestBase.py b/test/TestBase.py index 431aaa00..cf73cec2 100644 --- a/test/TestBase.py +++ b/test/TestBase.py @@ -9,6 +9,7 @@ Utils.local_path.cached_path = file_path from BaseClasses import MultiWorld, CollectionState from worlds.alttp.Items import ItemFactory + class TestBase(unittest.TestCase): world: MultiWorld _state_cache = {} diff --git a/test/base/__init__.py b/test/base/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/base/TestFill.py b/test/general/TestFill.py similarity index 98% rename from test/base/TestFill.py rename to test/general/TestFill.py index 38447b62..573a224f 100644 --- a/test/base/TestFill.py +++ b/test/general/TestFill.py @@ -1,4 +1,4 @@ -from typing import NamedTuple +from typing import NamedTuple, List import unittest from worlds.AutoWorld import World from Fill import FillError, fill_restrictive @@ -28,8 +28,8 @@ def generate_multi_world(players: int = 1) -> MultiWorld: class PlayerDefinition(NamedTuple): id: int menu: Region - locations: list[Location] - prog_items: list[Item] + locations: List[Location] + prog_items: List[Item] def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition: @@ -40,7 +40,7 @@ def generate_player_data(multi_world: MultiWorld, player_id: int, location_count return PlayerDefinition(player_id, menu, locations, prog_items) -def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> list[Location]: +def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> List[Location]: locations = [] for i in range(count): name = "player" + str(player_id) + "_location" + str(i) @@ -50,7 +50,7 @@ def generate_locations(count: int, player_id: int, address: int = None, region: return locations -def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> list[Location]: +def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]: items = [] for i in range(count): name = "player" + str(player_id) + "_item" + str(i) diff --git a/test/minor_glitches/TestMinor.py b/test/minor_glitches/TestMinor.py index ac277205..db77ee91 100644 --- a/test/minor_glitches/TestMinor.py +++ b/test/minor_glitches/TestMinor.py @@ -4,15 +4,15 @@ from BaseClasses import MultiWorld from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.EntranceShuffle import link_entrances from worlds.alttp.InvertedRegions import mark_dark_world_regions -from worlds.alttp.ItemPool import difficulties, generate_itempool +from worlds.alttp.ItemPool import difficulties from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import create_regions from worlds.alttp.Shops import create_shops -from worlds.alttp.Rules import set_rules from test.TestBase import TestBase from worlds import AutoWorld + class TestMinor(TestBase): def setUp(self): self.world = MultiWorld(1) @@ -30,8 +30,10 @@ class TestMinor(TestBase): self.world.worlds[1].create_items() self.world.required_medallions[1] = ['Ether', 'Quake'] self.world.itempool.extend(get_dungeon_item_pool(self.world)) - 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.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 mark_dark_world_regions(self.world, 1) - self.world.worlds[1].set_rules() \ No newline at end of file + self.world.worlds[1].set_rules() diff --git a/worlds/hk/__init__.py b/worlds/hk/__init__.py index 046ee6ea..1826ba93 100644 --- a/worlds/hk/__init__.py +++ b/worlds/hk/__init__.py @@ -68,16 +68,12 @@ class HKWorld(World): self.world.itempool += pool - def set_rules(self): set_rules(self.world, self.player) def create_regions(self): create_regions(self.world, self.player) - def generate_output(self): - pass # Hollow Knight needs no output files - def fill_slot_data(self): slot_data = {} for option_name in self.options: From d2e884b1d9e845048da362b04e99181cbb506aa6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 6 Jan 2022 06:18:54 +0100 Subject: [PATCH 03/24] Options: allow Toggles to be hashed --- Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Options.py b/Options.py index ef786e47..2bf329f1 100644 --- a/Options.py +++ b/Options.py @@ -125,6 +125,8 @@ class Toggle(Option): def get_option_name(cls, value): return ["No", "Yes"][int(value)] + __hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__ + class DefaultOnToggle(Toggle): default = 1 From 9894d0672f5708b784661527ff026d9a1ee64d72 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 6 Jan 2022 17:03:47 +0100 Subject: [PATCH 04/24] Options: allow Choices to be hashed --- Options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Options.py b/Options.py index 2bf329f1..a5ef5f9c 100644 --- a/Options.py +++ b/Options.py @@ -186,6 +186,8 @@ class Choice(Option): else: raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") + __hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__ + class Range(Option, int): range_start = 0 From aeda76c0586e1c5b556ea97a06168375e90b4f3e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 6 Jan 2022 19:49:26 +0100 Subject: [PATCH 05/24] WebHost: sort games by alphabet --- WebHostLib/templates/supportedGames.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index f6409916..7ed14ce0 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -9,7 +9,7 @@ {% include 'header/grassHeader.html' %}

Currently Supported Games

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

{{ game }}

Settings Page From d4c6268a462b863c178586913464a3f7db00b3b7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 6 Jan 2022 22:01:18 +0100 Subject: [PATCH 06/24] Generate: allow meta to log-fail as opposed to exception-fail if category is missing in target --- Generate.py | 2 +- host.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Generate.py b/Generate.py index 4704a59d..2d67e237 100644 --- a/Generate.py +++ b/Generate.py @@ -148,7 +148,7 @@ def main(args=None, callback=ERmain): if category_name is None: weights_cache[path][key] = option elif category_name not in weights_cache[path]: - raise Exception(f"Meta: Category {category_name} is not present in {path}.") + logging.warning(f"Meta: Category {category_name} is not present in {path}.") else: weights_cache[path][category_name][key] = option diff --git a/host.yaml b/host.yaml index 6411702a..1b2e6a53 100644 --- a/host.yaml +++ b/host.yaml @@ -64,7 +64,7 @@ generator: # general weights file, within the stated player_files_path location # gets used if players is higher than the amount of per-player files found to fill remaining slots weights_file_path: "weights.yaml" - # Meta file name, within the stated player_files_path location, TODO: re-implement this + # Meta file name, within the stated player_files_path location meta_file_path: "meta.yaml" # Create a spoiler file # 0 -> None From 969ea5e6ee398daf86836b9ba4ecf8e5c6ecc43b Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 6 Jan 2022 23:04:55 +0100 Subject: [PATCH 07/24] fix triggers for multiple slots from one yaml --- Generate.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Generate.py b/Generate.py index 2d67e237..295eb140 100644 --- a/Generate.py +++ b/Generate.py @@ -23,6 +23,7 @@ import Options from worlds.alttp import Bosses from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister +import copy categories = set(AutoWorldRegister.world_types) @@ -330,7 +331,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di def roll_linked_options(weights: dict) -> dict: - weights = weights.copy() # make sure we don't write back to other weights sets in same_settings + 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"]: if "name" not in option_set: raise ValueError("One of your linked options does not have a name.") @@ -352,7 +353,7 @@ def roll_linked_options(weights: dict) -> dict: def roll_triggers(weights: dict, triggers: list) -> dict: - weights = weights.copy() # make sure we don't write back to other weights sets in same_settings + weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings weights["_Generator_Version"] = Utils.__version__ for i, option_set in enumerate(triggers): try: From f6197d0a8dc714eec8be3ceca2e4540f9424ec4c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 7 Jan 2022 03:32:51 +0100 Subject: [PATCH 08/24] WebHost: add pretty print version of datapackage for human eyes --- WebHostLib/__init__.py | 9 +++++++++ WebHostLib/api/__init__.py | 1 + 2 files changed, 10 insertions(+) diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 085cfe56..847e99c8 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -193,6 +193,15 @@ 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") + + from WebHostLib.customserver import run_server_process from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it diff --git a/WebHostLib/api/__init__.py b/WebHostLib/api/__init__.py index 63483abc..e7029722 100644 --- a/WebHostLib/api/__init__.py +++ b/WebHostLib/api/__init__.py @@ -31,6 +31,7 @@ def get_datapackge(): from worlds import network_data_package return network_data_package + @api_endpoints.route('/datapackage_version') @cache.cached() def get_datapackge_versions(): From f8030393c88a908e8fae4166261ef1c673e5806f Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Fri, 7 Jan 2022 00:03:34 -0500 Subject: [PATCH 09/24] OoT: If skip_child_zelda is on, set rule on Song from Impa to be giveable item --- worlds/oot/Rules.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index ec44459b..7ef75640 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -140,6 +140,11 @@ def set_rules(ootworld): location = world.get_location('Sheik in Ice Cavern', player) add_item_rule(location, lambda item: item.player == player and item.type == 'Song') + if ootworld.skip_child_zelda: + # If skip child zelda is on, the item at Song from Impa must be giveable by the save context. + location = world.get_location('Song from Impa', player) + add_item_rule(location, lambda item: item in SaveContext.giveable_items) + for name in ootworld.always_hints: add_rule(world.get_location(name, player), guarantee_hint) From 340725d395e6309539196285f56aeb30aed7b787 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Fri, 7 Jan 2022 00:04:04 -0500 Subject: [PATCH 10/24] OoT: add protection on starting inventory to be only giveable items --- worlds/oot/__init__.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index ee936a44..20139d03 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -24,6 +24,7 @@ from .N64Patch import create_patch_file from .Cosmetics import patch_cosmetics from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints from .HintList import getRequiredHints +from .SaveContext import SaveContext from Utils import get_options, output_path from BaseClasses import MultiWorld, CollectionState, RegionType @@ -471,13 +472,16 @@ class OOTWorld(World): self.remove_from_start_inventory.remove(item.name) removed_items.append(item.name) else: - self.starting_items[item.name] += 1 - if item.type == 'Song': - self.starting_songs = True - # Call the junk fill and get a replacement - if item in self.itempool: - self.itempool.remove(item) - self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool))) + if item.name not in SaveContext.giveable_items: + raise Exception(f"Invalid OoT starting item: {item.name}") + else: + self.starting_items[item.name] += 1 + if item.type == 'Song': + self.starting_songs = True + # Call the junk fill and get a replacement + if item in self.itempool: + self.itempool.remove(item) + self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool))) if self.start_with_consumables: self.starting_items['Deku Sticks'] = 30 self.starting_items['Deku Nuts'] = 40 @@ -718,7 +722,6 @@ class OOTWorld(World): impa = self.world.get_location("Song from Impa", self.player) if self.skip_child_zelda: if impa.item is None: - from .SaveContext import SaveContext item_to_place = self.world.random.choice(list(item for item in self.world.itempool if item.player == self.player and item.name in SaveContext.giveable_items)) impa.place_locked_item(item_to_place) From 428af55bd9fb755ccb3488ce3b0824dee5329065 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 6 Jan 2022 23:42:55 -0500 Subject: [PATCH 11/24] LTTP shop price modifier tweak Ensure shop prices are a multiple of 5 after price modifier --- worlds/alttp/Shops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index a5d35b1b..29401117 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -264,7 +264,7 @@ def ShopSlotFill(world): price = world.random.randrange(8, 56) shop.push_inventory(location.shop_slot, item_name, - min(int(price * 5 * world.shop_price_modifier[location.player] / 100), 9999), 1, + min(int(price * world.shop_price_modifier[location.player] / 100) * 5, 9999), 1, location.item.player if location.item.player != location.player else 0) if 'P' in world.shop_shuffle[location.player]: price_to_funny_price(shop.inventory[location.shop_slot], world, location.player) From 967e3028fd4d32be5378f6df08d7ebf7dee2e1c8 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 7 Jan 2022 22:45:57 -0500 Subject: [PATCH 12/24] LTTP - Cap item prices at 4x I think quadrupled prices will be plenty expensive, and this will stop people who pick "random" from getting 9999 priced items and potentially locking their multiworld behind absurd rupee grinds --- worlds/alttp/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 962f1297..772026d7 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -95,7 +95,7 @@ class ShopPriceModifier(Range): """Percentage modifier for shuffled item prices in shops""" range_start = 0 default = 100 - range_end = 10000 + range_end = 400 class WorldState(Choice): option_standard = 1 From f656f08f9bffebb0b11b322de25fb1217c44eff6 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 5 Jan 2022 16:58:49 +0100 Subject: [PATCH 13/24] Docs: Cherry pick SM guide update from docs consolidation --- .../tutorial/super-metroid/multiworld_en.md | 127 ++++++++++-------- 1 file changed, 72 insertions(+), 55 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/super-metroid/multiworld_en.md b/WebHostLib/static/assets/tutorial/super-metroid/multiworld_en.md index 88f8b844..c88be628 100644 --- a/WebHostLib/static/assets/tutorial/super-metroid/multiworld_en.md +++ b/WebHostLib/static/assets/tutorial/super-metroid/multiworld_en.md @@ -1,126 +1,143 @@ # Super Metroid Setup Guide ## Required Software -- [Super Metroid Client](https://github.com/ArchipelagoMW/SuperMetroidClient/releases) -- **sniConnector.lua** (located on the client download page) -- [SNI](https://github.com/alttpo/sni/releases) (Included in the Super Metroid Client) + +- SNI Client + - Included in Archipelago download - Hardware or software capable of loading and playing SNES ROM files - - An emulator capable of connecting to SNI - ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), - [BizHawk](http://tasvideos.org/BizHawk.html)) - - An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware -- Your Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc` + - An emulator capable of connecting to SNI such as: + - snes9x Multitroid + from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), + - BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html) + - An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other + compatible hardware +- Your legally obtained Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc` ## Installation Procedures ### Windows Setup -1. Download and install the Super Metroid Client from the link above, making sure to install the most recent version. -**The file is located in the assets section at the bottom of the version information**. + +1. During the installation of Archipelago, you will have been asked to install the SNI Client. + If you did not do this, or you are on an older version, you may run the installer again to install the SNI Client. 2. During setup, you will be asked to locate your base ROM file. This is your Super Metroid ROM file. -3. If you are using an emulator, you should assign your Lua capable emulator as your default program -for launching ROM files. - 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. 2. Right-click on a ROM file and select **Open with...** 3. Check the box next to **Always use this app to open .sfc files** 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** - 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside - the folder you extracted in step one. + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. ### Macintosh Setup + - We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help. ## Create a Config (.yaml) File ### What is a config file and why do I need one? -Your config file contains a set of configuration options which provide the generator with information about how -it should generate your game. Each player of a multiworld will provide their own config file. This setup allows -each player to enjoy an experience customized for their taste, and different players in the same multiworld -can all have different options. + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en) ### Where do I get a config file? -The [Player Settings](/games/Super%20Metroid/player-settings) page on the website allows you to configure your -personal settings and export a config file from them. + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings) ### Verifying your config file -If you would like to validate your config file to make sure it works, you may do so on the -[YAML Validator](/mysterycheck) page. + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) ## Generating a Single-Player Game -1. Navigate to the [Player Settings](/games/Super%20Metroid/player-settings) page, configure your options, and click - the "Generate Game" button. + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. -5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from - the patch file, and open your emulator for you. +5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from the + patch file, and open your emulator for you. 6. Since this is a single-player game, you will no longer need the client, so feel free to close it. ## Joining a MultiWorld Game ### Obtain your patch file and create your ROM -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 `.apm3` 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. +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 `.apm3` 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. ### Connect to the client #### With an emulator -When the client launched automatically, SNI should have also automatically launched in the background. -If this is its first time launching, you may be prompted to allow it to communicate through the Windows -Firewall. + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. ##### snes9x Multitroid + 1. Load your ROM file if it hasn't already been loaded. 2. Click on the File menu and hover on **Lua Scripting** 3. Click on **New Lua Script Window...** 4. In the new window, click **Browse...** -5. Select the `sniConnector.lua` file you downloaded above +5. Select the `Connector.lua` file in the `Archipelago\SNI\lua` folder. + - Use x86 for 32-bit or x64 for 64-bit. ##### BizHawk -1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following - these menu options: + +1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these + menu options: `Config --> Cores --> SNES --> BSNES` Once you have changed the loaded core, you must restart BizHawk. 2. Load your ROM file if it hasn't already been loaded. 3. Click on the Tools menu and click on **Lua Console** 4. Click the button to open a new Lua script. -5. Select the `sniConnector.lua` file you downloaded above +5. Select the `Connector.lua` file in `Archipelago\SNI\lua` folder. + - Use x86 for 32-bit or x64 for 64-bit. Please note the most recent versions of BizHawk are 64-bit only. #### With hardware -This guide assumes you have downloaded the correct firmware for your device. If you have not -done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware -[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information -[on this page](http://usb2snes.com/#supported-platforms). + +This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do +this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES +releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases) + +Other hardware may find helpful information on the usb2snes platforms +page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms) 1. Close your emulator, which may have auto-launched. 2. Power on your device and load the ROM. ### Connect to the Archipelago Server -The patch file which launched your client should have automatically connected you to the AP Server. -There are a few reasons this may not happen however, including if the game is hosted on the website but -was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host -for the address of the server, and copy/paste it into the "Server" input field then press enter. -The client will attempt to reconnect to the new server address, and should momentarily show "Server -Status: Connected". +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". ### Play the game -When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations -on successfully joining a multiworld game! + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! ## Hosting a MultiWorld game -The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple: + +The recommended way to host a game is to use our hosting service. The process is relatively simple: 1. Collect config files from your players. 2. Create a zip file containing your players' config files. -3. Upload that zip file to the website linked above. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) 4. Wait a moment while the seed is generated. 5. When the seed is generated, you will be redirected to a "Seed Info" page. -6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, - so they may download their patch files from there. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. 7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all players in the game. Any observers may also be given the link to this page. 8. Once all players have joined, you may begin playing. From aff9114c35deaacd7bcfc19a745c1a151ead1e7c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 8 Jan 2022 16:12:56 +0100 Subject: [PATCH 14/24] 0.2.3 --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index df5d3c96..c2eab405 100644 --- a/Utils.py +++ b/Utils.py @@ -23,7 +23,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.2.2" +__version__ = "0.2.3" version_tuple = tuplize_version(__version__) from yaml import load, dump, safe_load From 82e180cca86673a3c1d76f664e942e4306d3597c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 8 Jan 2022 17:11:39 +0100 Subject: [PATCH 15/24] WebHost: mark slot counts as exact, now that an entry for each slot is created in DB --- WebHostLib/templates/userContent.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html index 6d32f100..3e34c0e3 100644 --- a/WebHostLib/templates/userContent.html +++ b/WebHostLib/templates/userContent.html @@ -31,7 +31,7 @@ {{ room.seed.id|suuid }} {{ room.id|suuid }} - >={{ room.seed.slots|length }} + {{ room.seed.slots|length }} {{ room.creation_time.strftime("%Y-%m-%d %H:%M") }} {{ room.last_activity.strftime("%Y-%m-%d %H:%M") }} @@ -56,7 +56,7 @@ {% for seed in seeds %} {{ seed.id|suuid }} - {% if seed.multidata %}>={{ seed.slots|length }}{% else %}1{% endif %} + {% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %} {{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }} From 4909479c427f1aef51944404c33f37b35726d07e Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 8 Jan 2022 13:49:58 -0600 Subject: [PATCH 16/24] Slay the Spire: Wrote a basic set-up guide and info guide for StS --- .../assets/gameInfo/en_Slay the Spire.md | 29 +++++++++++++++++ .../slay-the-spire/slay-the-spire_en.md | 32 +++++++++++++++++++ .../static/assets/tutorial/tutorials.json | 19 +++++++++++ 3 files changed, 80 insertions(+) create mode 100644 WebHostLib/static/assets/gameInfo/en_Slay the Spire.md create mode 100644 WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md diff --git a/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md b/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md new file mode 100644 index 00000000..a66af04f --- /dev/null +++ b/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md @@ -0,0 +1,29 @@ +# Slay the Spire (PC) + +## Where is the settings page? +The player settings page for this game is located here. It contains all the options +you need to configure and export a config file. + +## What does randomization do to this game? +Every non-boss relic drop, every boss relic and rare card drop, and every other card draw is replaced with an +archipelago item. In heart runs, the blue key is also disconnected from the Archipelago item, so you can gather both. + +## What items and locations get shuffled? +15 card packs, 10 relics, and 3 boss relics and rare card drops are shuffled into the item pool and can be found at any +location that would normally give you these items, except for card packs, which are found at every other normal enemy +encounter. + +## Which items can be in another player's world? +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to +limit certain items to your own world. + +## When the player receives an item, what happens? +When the player receives an item, you will see the counter in the top right corner with the Archipelago symbol increment +by one. By clicking on this icon, it'll open a menu that lists all the items you received, but have not yet accepted. +You can take any relics and card packs sent to you and add them to your current run. It is advised that you do not open +this menu until you are outside an encounter or event to prevent the game from soft-locking. + +## What happens if a player dies in a run? Do they have to start completely over? +When a player dies, they will be taken back to the main menu and will need to reconnect to start climbing the spire +from the beginning, but they will have access to all the items ever sent to them in the Archipelago menu in the top +right. Any items found in an earlier run will not be sent again if you encounter them in the same location. diff --git a/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md b/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md new file mode 100644 index 00000000..55100a71 --- /dev/null +++ b/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md @@ -0,0 +1,32 @@ +# Slay the Spire Setup Guide + +## Required Software + +For steam-based installation, subscribe to the following mods: + +- ModTheSpire from the [Slay the Spire Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=1605060445) +- BaseMod from the [Slay the Spire Workshop](https://steamcommunity.com/workshop/filedetails/?id=1605833019) +- Archipelago Multiworld Randomizer Mod from the [Slay the Spire Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2596397288) + +## Configuring your YAML file + +### What is a YAML file and why do I need one? +Your YAML file contains a set of configuration options which provide the generator with information about how +it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows +each player to enjoy an experience customized for their taste, and different players in the same multiworld +can all have different options. + +### Where do I get a YAML file? +you can customize your settings by visiting the [Rogue Legacy Settings Page](/games/Rogue%20Legacy/player-settings). + +### Connect to the MultiServer +For Steam-based installations, if you are subscribed to ModTheSpire, when you launch the game, you should have the +option to launch the game with mods. On the mod loader screen, ensure you only have the following mods enabled and then +start the game: + +- BaseMod +- Archipelago Multiworld Randomizer + +Once you are in-game, you will be able to click the **Archipelago** menu option and enter the ip and port (separated by +a colon) in the hostname field and enter your player slot name in the Slot Name field. Then click connect, and now you +are ready to climb the spire! diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index 5cc02658..67aa2086 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -361,5 +361,24 @@ ] } ] + }, + { + "gameTitle": "Slay the Spire", + "tutorials": [ + { + "name": "Multiworld Setup Guide", + "description": "A guide to setting up Slay the Spire for Archipelago. This guide covers single-player, multiworld, and related software.", + "files": [ + { + "language": "English", + "filename": "slay-the-spire/slay-the-spire_en.md", + "link": "slay-the-spire/slay-the-spire/en", + "authors": [ + "Phar" + ] + } + ] + } + ] } ] From 68f282ee8325b06b093baace8b301de040c6ea60 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 8 Jan 2022 19:54:53 +0000 Subject: [PATCH 17/24] Slay the Spire: Removed redundant sentence --- WebHostLib/static/assets/gameInfo/en_Slay the Spire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md b/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md index a66af04f..9ec04834 100644 --- a/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md +++ b/WebHostLib/static/assets/gameInfo/en_Slay the Spire.md @@ -23,7 +23,7 @@ by one. By clicking on this icon, it'll open a menu that lists all the items you You can take any relics and card packs sent to you and add them to your current run. It is advised that you do not open this menu until you are outside an encounter or event to prevent the game from soft-locking. -## What happens if a player dies in a run? Do they have to start completely over? +## What happens if a player dies in a run? When a player dies, they will be taken back to the main menu and will need to reconnect to start climbing the spire from the beginning, but they will have access to all the items ever sent to them in the Archipelago menu in the top right. Any items found in an earlier run will not be sent again if you encounter them in the same location. From 0472147e9aa9bd3755616eb6b4db5bfe42633e57 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 8 Jan 2022 19:57:34 +0000 Subject: [PATCH 18/24] Forgot to update link --- .../static/assets/tutorial/slay-the-spire/slay-the-spire_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md b/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md index 55100a71..9ff2bc32 100644 --- a/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md +++ b/WebHostLib/static/assets/tutorial/slay-the-spire/slay-the-spire_en.md @@ -17,7 +17,7 @@ each player to enjoy an experience customized for their taste, and different pla can all have different options. ### Where do I get a YAML file? -you can customize your settings by visiting the [Rogue Legacy Settings Page](/games/Rogue%20Legacy/player-settings). +you can customize your settings by visiting the [Slay the Spire Settings Page](/games/Slay%20the%20Spire/player-settings). ### Connect to the MultiServer For Steam-based installations, if you are subscribed to ModTheSpire, when you launch the game, you should have the From 7e32fa1311793d21ed291891e6d90fe7f76fe0bd Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 8 Jan 2022 21:21:29 +0100 Subject: [PATCH 19/24] WebHost: fix uploading .archipelago files --- WebHostLib/upload.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 607b0aff..d1911a92 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -108,12 +108,13 @@ def uploads(): elif res: return redirect(url_for("view_seed", seed=res.id)) else: + file.seek(0) # offset from is_zipfile check # noinspection PyBroadException try: multidata = file.read() MultiServer.Context.decompress(multidata) - except: - flash("Could not load multidata. File may be corrupted or incompatible.") + except Exception as e: + flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})") else: seed = Seed(multidata=multidata, owner=session["_id"]) flush() # place into DB and generate ids From 7380df0256746f55577e0191048dec4f84e7c626 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sat, 8 Jan 2022 16:59:39 -0500 Subject: [PATCH 20/24] [WebHost] weighted-settings: Add Item Management section, currently non-functional --- WebHostLib/static/assets/weighted-settings.js | 249 ++++++++++++------ .../static/styles/weighted-settings.css | 32 +++ 2 files changed, 199 insertions(+), 82 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 91789c2f..496b0c15 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -128,19 +128,22 @@ const buildUI = (settingData) => { expandButton.classList.add('invisible'); gameDiv.appendChild(expandButton); - const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings); - gameDiv.appendChild(optionsDiv); + const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings); + gameDiv.appendChild(weightedSettingsDiv); + + const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems); + gameDiv.appendChild(itemsDiv); gamesWrapper.appendChild(gameDiv); collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); - optionsDiv.classList.add('invisible'); + weightedSettingsDiv.classList.add('invisible'); expandButton.classList.remove('invisible'); }); expandButton.addEventListener('click', () => { collapseButton.classList.remove('invisible'); - optionsDiv.classList.remove('invisible'); + weightedSettingsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); }); @@ -207,10 +210,10 @@ const buildGameChoice = (games) => { gameChoiceDiv.appendChild(table); }; -const buildOptionsDiv = (game, settings) => { +const buildWeightedSettingsDiv = (game, settings) => { const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); - const optionsWrapper = document.createElement('div'); - optionsWrapper.classList.add('settings-wrapper'); + const settingsWrapper = document.createElement('div'); + settingsWrapper.classList.add('settings-wrapper'); Object.keys(settings).forEach((settingName) => { const setting = settings[settingName]; @@ -268,27 +271,6 @@ const buildOptionsDiv = (game, settings) => { break; case 'range': - const hintText = document.createElement('p'); - hintText.classList.add('hint-text'); - hintText.innerHTML = 'This is a range option. You may enter valid numerical values in the text box below, ' + - `then press the "Add" button to add a weight for it.
Minimum value: ${setting.min}
` + - `Maximum value: ${setting.max}`; - settingWrapper.appendChild(hintText); - - const addOptionDiv = document.createElement('div'); - addOptionDiv.classList.add('add-option-div'); - const optionInput = document.createElement('input'); - optionInput.setAttribute('id', `${game}-${settingName}-option`); - optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); - addOptionDiv.appendChild(optionInput); - const addOptionButton = document.createElement('button'); - addOptionButton.innerText = 'Add'; - addOptionDiv.appendChild(addOptionButton); - settingWrapper.appendChild(addOptionDiv); - optionInput.addEventListener('keydown', (evt) => { - if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } - }); - const rangeTable = document.createElement('table'); const rangeTbody = document.createElement('tbody'); @@ -324,6 +306,79 @@ const buildOptionsDiv = (game, settings) => { rangeTbody.appendChild(tr); } } else { + const hintText = document.createElement('p'); + hintText.classList.add('hint-text'); + hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' + + `below, then press the "Add" button to add a weight for it.
Minimum value: ${setting.min}
` + + `Maximum value: ${setting.max}`; + settingWrapper.appendChild(hintText); + + const addOptionDiv = document.createElement('div'); + addOptionDiv.classList.add('add-option-div'); + const optionInput = document.createElement('input'); + optionInput.setAttribute('id', `${game}-${settingName}-option`); + optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`); + addOptionDiv.appendChild(optionInput); + const addOptionButton = document.createElement('button'); + addOptionButton.innerText = 'Add'; + addOptionDiv.appendChild(addOptionButton); + settingWrapper.appendChild(addOptionDiv); + optionInput.addEventListener('keydown', (evt) => { + if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); } + }); + + addOptionButton.addEventListener('click', () => { + const optionInput = document.getElementById(`${game}-${settingName}-option`); + let option = optionInput.value; + if (!option || !option.trim()) { return; } + option = parseInt(option, 10); + if ((option < setting.min) || (option > setting.max)) { return; } + optionInput.value = ''; + if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; } + + const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = option; + tr.appendChild(tdLeft); + + const tdMiddle = document.createElement('td'); + tdMiddle.classList.add('td-middle'); + const range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('id', `${game}-${settingName}-${option}-range`); + range.setAttribute('data-game', game); + range.setAttribute('data-setting', settingName); + range.setAttribute('data-option', option); + range.setAttribute('min', 0); + range.setAttribute('max', 50); + range.addEventListener('change', updateGameSetting); + range.value = currentSettings[game][settingName][parseInt(option, 10)]; + tdMiddle.appendChild(range); + tr.appendChild(tdMiddle); + + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${game}-${settingName}-${option}`) + tdRight.classList.add('td-right'); + tdRight.innerText = range.value; + tr.appendChild(tdRight); + + const tdDelete = document.createElement('td'); + tdDelete.classList.add('td-delete'); + const deleteButton = document.createElement('span'); + deleteButton.classList.add('range-option-delete'); + deleteButton.innerText = '❌'; + deleteButton.addEventListener('click', () => { + range.value = 0; + range.dispatchEvent(new Event('change')); + rangeTbody.removeChild(tr); + }); + tdDelete.appendChild(deleteButton); + tr.appendChild(tdDelete); + + rangeTbody.appendChild(tr); + }); + Object.keys(currentSettings[game][settingName]).forEach((option) => { if (currentSettings[game][settingName][option] > 0) { const tr = document.createElement('tr'); @@ -403,58 +458,6 @@ const buildOptionsDiv = (game, settings) => { rangeTable.appendChild(rangeTbody); settingWrapper.appendChild(rangeTable); - - addOptionButton.addEventListener('click', () => { - const optionInput = document.getElementById(`${game}-${settingName}-option`); - let option = optionInput.value; - if (!option || !option.trim()) { return; } - option = parseInt(option, 10); - if ((option < setting.min) || (option > setting.max)) { return; } - optionInput.value = ''; - if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; } - - const tr = document.createElement('tr'); - const tdLeft = document.createElement('td'); - tdLeft.classList.add('td-left'); - tdLeft.innerText = option; - tr.appendChild(tdLeft); - - const tdMiddle = document.createElement('td'); - tdMiddle.classList.add('td-middle'); - const range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('id', `${game}-${settingName}-${option}-range`); - range.setAttribute('data-game', game); - range.setAttribute('data-setting', settingName); - range.setAttribute('data-option', option); - range.setAttribute('min', 0); - range.setAttribute('max', 50); - range.addEventListener('change', updateGameSetting); - range.value = currentSettings[game][settingName][parseInt(option, 10)]; - tdMiddle.appendChild(range); - tr.appendChild(tdMiddle); - - const tdRight = document.createElement('td'); - tdRight.setAttribute('id', `${game}-${settingName}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); - - const tdDelete = document.createElement('td'); - tdDelete.classList.add('td-delete'); - const deleteButton = document.createElement('span'); - deleteButton.classList.add('range-option-delete'); - deleteButton.innerText = '❌'; - deleteButton.addEventListener('click', () => { - range.value = 0; - range.dispatchEvent(new Event('change')); - rangeTbody.removeChild(tr); - }); - tdDelete.appendChild(deleteButton); - tr.appendChild(tdDelete); - - rangeTbody.appendChild(tr); - }); break; default: @@ -462,10 +465,92 @@ const buildOptionsDiv = (game, settings) => { return; } - optionsWrapper.appendChild(settingWrapper); + settingsWrapper.appendChild(settingWrapper); }); - return optionsWrapper; + return settingsWrapper; +}; + +const buildItemsDiv = (game, items) => { + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + const itemsDiv = document.createElement('div'); + itemsDiv.classList.add('items-div'); + + const itemsDivHeader = document.createElement('h3'); + itemsDivHeader.innerText = 'Item Management'; + itemsDiv.appendChild(itemsDivHeader); + + const itemsDescription = document.createElement('p'); + itemsDescription.classList.add('setting-description'); + itemsDescription.innerText = 'Choose if you would like to start with items, or control if they are placed in ' + + 'your seed or someone else\'s.'; + itemsDiv.appendChild(itemsDescription); + + const itemsHint = document.createElement('p'); + itemsHint.classList.add('hint-text'); + itemsHint.innerText = 'Drag and drop items from one box to another.'; + itemsDiv.appendChild(itemsHint); + + const itemsWrapper = document.createElement('div'); + itemsWrapper.classList.add('items-wrapper'); + + // Create container divs for each category + const availableItemsWrapper = document.createElement('div'); + availableItemsWrapper.classList.add('item-set-wrapper'); + availableItemsWrapper.innerText = 'Available Items'; + const availableItems = document.createElement('div'); + availableItems.classList.add('item-container'); + availableItems.setAttribute('id', `${game}-available-items`); + + const startInventoryWrapper = document.createElement('div'); + startInventoryWrapper.classList.add('item-set-wrapper'); + startInventoryWrapper.innerText = 'Start Inventory'; + const startInventory = document.createElement('div'); + startInventory.classList.add('item-container'); + startInventory.setAttribute('id', `${game}-start-inventory`); + + const localItemsWrapper = document.createElement('div'); + localItemsWrapper.classList.add('item-set-wrapper'); + localItemsWrapper.innerText = 'Local Items'; + const localItems = document.createElement('div'); + localItems.classList.add('item-container'); + localItems.setAttribute('id', `${game}-local-items`); + + const nonLocalItemsWrapper = document.createElement('div'); + nonLocalItemsWrapper.classList.add('item-set-wrapper'); + nonLocalItemsWrapper.innerText = 'Non-Local Items'; + const nonLocalItems = document.createElement('div'); + nonLocalItems.classList.add('item-container'); + nonLocalItems.setAttribute('id', `${game}-remote-items`); + + // Populate the divs + items.sort().forEach((item) => { + const itemDiv = document.createElement('div'); + itemDiv.classList.add('item-div'); + itemDiv.setAttribute('id', `${game}-${item}`); + itemDiv.innerText = item; + + if (currentSettings[game].start_inventory.includes(item)){ + startInventory.appendChild(itemDiv); + } else if (currentSettings[game].local_items.includes(item)) { + localItems.appendChild(itemDiv); + } else if (currentSettings[game].non_local_items.includes(item)) { + nonLocalItems.appendChild(itemDiv); + } else { + availableItems.appendChild(itemDiv); + } + }); + + availableItemsWrapper.appendChild(availableItems); + startInventoryWrapper.appendChild(startInventory); + localItemsWrapper.appendChild(localItems); + nonLocalItemsWrapper.appendChild(nonLocalItems); + itemsWrapper.appendChild(availableItemsWrapper); + itemsWrapper.appendChild(startInventoryWrapper); + itemsWrapper.appendChild(localItemsWrapper); + itemsWrapper.appendChild(nonLocalItemsWrapper); + itemsDiv.appendChild(itemsWrapper); + return itemsDiv; }; const updateVisibleGames = () => { diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index ae488aff..28ab4a10 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -90,6 +90,38 @@ html{ cursor: pointer; } +#weighted-settings .items-wrapper{ + display: flex; + flex-direction: row; + justify-content: space-between; +} + +#weighted-settings .items-div h3{ + margin-bottom: 0.5rem; +} + +#weighted-settings .items-wrapper .item-set-wrapper{ + width: 24%; +} + +#weighted-settings .items-wrapper .item-container{ + border: 1px solid #ffffff; + border-radius: 2px; + width: 100%; + height: 300px; + overflow-y: auto; + overflow-x: hidden; +} + +#weighted-settings .items-wrapper .item-container .item-div{ + padding: 0.15rem; + cursor: pointer; +} + +#weighted-settings .items-wrapper .item-container .item-div:hover{ + background-color: rgba(0, 0, 0, 0.1); +} + #weighted-settings #weighted-settings-button-row{ display: flex; flex-direction: row; From 9ff3791d9ea83244852a4e144c546f17b998cf54 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sat, 8 Jan 2022 19:59:35 -0500 Subject: [PATCH 21/24] [WebHost] weighted-settings: Implement Item Pool settings --- WebHostLib/static/assets/weighted-settings.js | 91 +++++++++++++++++-- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 496b0c15..7b37f37c 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -138,12 +138,14 @@ const buildUI = (settingData) => { collapseButton.addEventListener('click', () => { collapseButton.classList.add('invisible'); weightedSettingsDiv.classList.add('invisible'); + itemsDiv.classList.add('invisible'); expandButton.classList.remove('invisible'); }); expandButton.addEventListener('click', () => { collapseButton.classList.remove('invisible'); weightedSettingsDiv.classList.remove('invisible'); + itemsDiv.classList.remove('invisible'); expandButton.classList.add('invisible'); }); }); @@ -477,7 +479,7 @@ const buildItemsDiv = (game, items) => { itemsDiv.classList.add('items-div'); const itemsDivHeader = document.createElement('h3'); - itemsDivHeader.innerText = 'Item Management'; + itemsDivHeader.innerText = 'Item Pool'; itemsDiv.appendChild(itemsDivHeader); const itemsDescription = document.createElement('p'); @@ -500,41 +502,52 @@ const buildItemsDiv = (game, items) => { availableItemsWrapper.innerText = 'Available Items'; const availableItems = document.createElement('div'); availableItems.classList.add('item-container'); - availableItems.setAttribute('id', `${game}-available-items`); + availableItems.setAttribute('id', `${game}-available_items`); + availableItems.addEventListener('dragover', itemDragoverHandler); + availableItems.addEventListener('drop', itemDropHandler); const startInventoryWrapper = document.createElement('div'); startInventoryWrapper.classList.add('item-set-wrapper'); startInventoryWrapper.innerText = 'Start Inventory'; const startInventory = document.createElement('div'); startInventory.classList.add('item-container'); - startInventory.setAttribute('id', `${game}-start-inventory`); + startInventory.setAttribute('id', `${game}-start_inventory`); + startInventory.setAttribute('data-setting', 'start_inventory'); + startInventory.addEventListener('dragover', itemDragoverHandler); + startInventory.addEventListener('drop', itemDropHandler); const localItemsWrapper = document.createElement('div'); localItemsWrapper.classList.add('item-set-wrapper'); localItemsWrapper.innerText = 'Local Items'; const localItems = document.createElement('div'); localItems.classList.add('item-container'); - localItems.setAttribute('id', `${game}-local-items`); + localItems.setAttribute('id', `${game}-local_items`); + localItems.setAttribute('data-setting', 'local_items') + localItems.addEventListener('dragover', itemDragoverHandler); + localItems.addEventListener('drop', itemDropHandler); const nonLocalItemsWrapper = document.createElement('div'); nonLocalItemsWrapper.classList.add('item-set-wrapper'); nonLocalItemsWrapper.innerText = 'Non-Local Items'; const nonLocalItems = document.createElement('div'); nonLocalItems.classList.add('item-container'); - nonLocalItems.setAttribute('id', `${game}-remote-items`); + nonLocalItems.setAttribute('id', `${game}-non_local_items`); + nonLocalItems.setAttribute('data-setting', 'non_local_items'); + nonLocalItems.addEventListener('dragover', itemDragoverHandler); + nonLocalItems.addEventListener('drop', itemDropHandler); // Populate the divs items.sort().forEach((item) => { - const itemDiv = document.createElement('div'); - itemDiv.classList.add('item-div'); - itemDiv.setAttribute('id', `${game}-${item}`); - itemDiv.innerText = item; + const itemDiv = buildItemDiv(game, item); if (currentSettings[game].start_inventory.includes(item)){ + itemDiv.setAttribute('data-setting', 'start_inventory'); startInventory.appendChild(itemDiv); } else if (currentSettings[game].local_items.includes(item)) { + itemDiv.setAttribute('data-setting', 'local_items'); localItems.appendChild(itemDiv); } else if (currentSettings[game].non_local_items.includes(item)) { + itemDiv.setAttribute('data-setting', 'non_local_items'); nonLocalItems.appendChild(itemDiv); } else { availableItems.appendChild(itemDiv); @@ -553,6 +566,66 @@ const buildItemsDiv = (game, items) => { return itemsDiv; }; +const buildItemDiv = (game, item) => { + const itemDiv = document.createElement('div'); + itemDiv.classList.add('item-div'); + itemDiv.setAttribute('id', `${game}-${item}`); + itemDiv.setAttribute('data-game', game); + itemDiv.setAttribute('data-item', item); + itemDiv.setAttribute('draggable', 'true'); + itemDiv.innerText = item; + itemDiv.addEventListener('dragstart', (evt) => { + evt.dataTransfer.setData('text/plain', itemDiv.getAttribute('id')); + }); + return itemDiv; +}; + +const itemDragoverHandler = (evt) => { + evt.preventDefault(); +}; + +const itemDropHandler = (evt) => { + evt.preventDefault(); + const sourceId = evt.dataTransfer.getData('text/plain'); + const sourceDiv = document.getElementById(sourceId); + + const currentSettings = JSON.parse(localStorage.getItem('weighted-settings')); + const game = sourceDiv.getAttribute('data-game'); + const item = sourceDiv.getAttribute('data-item'); + const itemDiv = buildItemDiv(game, item); + + const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null; + const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null; + + if (oldSetting) { + console.log(oldSetting); + console.log(item); + console.log(currentSettings[game][oldSetting].indexOf(item)); + console.log(currentSettings[game][oldSetting]); + if (currentSettings[game][oldSetting].includes(item)) { + currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1); + console.log(currentSettings[game][oldSetting]); + } + } + + if (newSetting) { + itemDiv.setAttribute('data-setting', newSetting); + document.getElementById(`${game}-${newSetting}`).appendChild(itemDiv); + if (!currentSettings[game][newSetting].includes(item)){ + currentSettings[game][newSetting].push(item); + } + } else { + // No setting was assigned, this item has been removed from the settings + document.getElementById(`${game}-available_items`).appendChild(itemDiv); + } + + // Remove the source drag object + sourceDiv.parentElement.removeChild(sourceDiv); + + // Save the updated settings + localStorage.setItem('weighted-settings', JSON.stringify(currentSettings)); +}; + const updateVisibleGames = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); Object.keys(settings.game).forEach((game) => { From 111b7e204f9cf8577240ce71c5f164444dfb61a7 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sat, 8 Jan 2022 20:34:19 -0500 Subject: [PATCH 22/24] [WebHost] weighted-settings: Remove debug output --- WebHostLib/static/assets/weighted-settings.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 7b37f37c..7d0a9698 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -598,13 +598,8 @@ const itemDropHandler = (evt) => { const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null; if (oldSetting) { - console.log(oldSetting); - console.log(item); - console.log(currentSettings[game][oldSetting].indexOf(item)); - console.log(currentSettings[game][oldSetting]); if (currentSettings[game][oldSetting].includes(item)) { currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1); - console.log(currentSettings[game][oldSetting]); } } From 651e22b14a6d5990759d8c77e8fa7e6d71e8a8eb Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 9 Jan 2022 04:32:17 +0100 Subject: [PATCH 23/24] LttP: keep Small Key Hyrule Castle local even if keyshuffle is wished. --- worlds/alttp/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 62cf4dec..3c075c8d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -350,16 +350,16 @@ class ALTTPWorld(World): # Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots # TODO: this might be worthwhile to introduce as generic option for various games and then optimize it if standard_keyshuffle_players: - viable = [] + viable = {} for location in world.get_locations(): if location.player in standard_keyshuffle_players \ and location.item is None \ and location.can_reach(world.state): - viable.append(location) - world.random.shuffle(viable) + viable.setdefault(location.player, []).append(location) + for player in standard_keyshuffle_players: + loc = world.random.choice(viable[player]) key = world.create_item("Small Key (Hyrule Castle)", player) - loc = viable.pop() loc.place_locked_item(key) fill_locations.remove(loc) world.random.shuffle(fill_locations) From bde58fb677443b6eed541fb6bf252e4248f1a325 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 9 Jan 2022 04:48:31 +0100 Subject: [PATCH 24/24] LttP: remove "bonus" small key hyrule castle in case of standard + own_dungeons --- worlds/alttp/Dungeons.py | 2 +- worlds/alttp/__init__.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 4b0814d3..cca78999 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -3,7 +3,7 @@ from worlds.alttp.Bosses import BossFactory from Fill import fill_restrictive from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import lookup_boss_drops -from worlds.alttp.Options import smallkey_shuffle +from worlds.alttp.Options import smallkey_shuffle def create_dungeons(world, player): diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3c075c8d..9cb46ff9 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -336,7 +336,8 @@ class ALTTPWorld(World): standard_keyshuffle_players = set() for player in world.get_game_players("A Link to the Past"): if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \ - and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal: + and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal and \ + world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons: standard_keyshuffle_players.add(player) if not world.ganonstower_vanilla[player] or \ world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}: @@ -364,9 +365,17 @@ class ALTTPWorld(World): fill_locations.remove(loc) world.random.shuffle(fill_locations) # TODO: investigate not creating the key in the first place - progitempool[:] = [item for item in progitempool if - item.player not in standard_keyshuffle_players or - item.name != "Small Key (Hyrule Castle)"] + if __debug__: + # keeping this here while I'm not sure we caught all instances of multiple HC small keys in the pool + count = len(progitempool) + progitempool[:] = [item for item in progitempool if + item.player not in standard_keyshuffle_players or + item.name != "Small Key (Hyrule Castle)"] + assert len(progitempool) + len(standard_keyshuffle_players) == count + else: + progitempool[:] = [item for item in progitempool if + item.player not in standard_keyshuffle_players or + item.name != "Small Key (Hyrule Castle)"] if trash_counts: locations_mapping = {player: [] for player in trash_counts}