From b04b105bd8b1b958d7ac0dcb47f6a470885722bf Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 29 May 2023 01:17:30 +0200 Subject: [PATCH 01/10] LADX: use custom collect/remove to keep track of logical rupee counts instead of LogixMixin May contain some pep8, sorry --- worlds/ladx/Items.py | 5 +++++ worlds/ladx/Locations.py | 31 +++++++++++-------------------- worlds/ladx/__init__.py | 39 +++++++++++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index e36a589c2c..d52eb7710b 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -32,12 +32,16 @@ class DungeonItemData(ItemData): s = self.ladxr_id[:-1] return DungeonItemType.__dict__[s] + class TradeItemData(ItemData): vanilla_location = None + def __new__(cls, item_name, ladxr_id, classification, vanilla_location): self = super(ItemData, cls).__new__(cls, (item_name, ladxr_id, classification)) self.vanilla_location = vanilla_location return self + + class LinksAwakeningItem(Item): game: str = Common.LINKS_AWAKENING @@ -49,6 +53,7 @@ class LinksAwakeningItem(Item): super().__init__(item_data.item_name, classification, Common.BASE_ID + item_data.item_id, player) self.item_data = item_data + # TODO: use _NAMES instead? class ItemName: POWER_BRACELET = "Progressive Power Bracelet" diff --git a/worlds/ladx/Locations.py b/worlds/ladx/Locations.py index 69eb78dd88..61e46fc0f4 100644 --- a/worlds/ladx/Locations.py +++ b/worlds/ladx/Locations.py @@ -1,12 +1,11 @@ -from BaseClasses import Region, Entrance, Location -from worlds.AutoWorld import LogicMixin +from BaseClasses import Region, Entrance, Location, CollectionState from .LADXR.checkMetadata import checkMetadataTable from .Common import * from worlds.generic.Rules import add_item_rule -from .Items import ladxr_item_to_la_item_name, ItemName, LinksAwakeningItem -from .LADXR.locations.tradeSequence import TradeRequirements, TradeSequenceItem +from .Items import ladxr_item_to_la_item_name + prefilled_events = ["ANGLER_KEYHOLE", "RAFT", "MEDICINE2", "CASTLE_BUTTON"] @@ -80,27 +79,19 @@ class LinksAwakeningLocation(Location): add_item_rule(self, filter_item) -def has_free_weapon(state: "CollectionState", player: int) -> bool: +def has_free_weapon(state: CollectionState, player: int) -> bool: return state.has("Progressive Sword", player) or state.has("Magic Rod", player) or state.has("Boomerang", player) or state.has("Hookshot", player) + # If the player has access to farm enough rupees to afford a game, we assume that they can keep beating the game -def can_farm_rupees(state: "CollectionState", player: int) -> bool: +def can_farm_rupees(state: CollectionState, player: int) -> bool: return has_free_weapon(state, player) and (state.has("Can Play Trendy Game", player=player) or state.has("RAFT", player=player)) -class LinksAwakeningLogic(LogicMixin): - rupees = { - ItemName.RUPEES_20: 0, - ItemName.RUPEES_50: 0, - ItemName.RUPEES_100: 100, - ItemName.RUPEES_200: 200, - ItemName.RUPEES_500: 500, - } - - def get_credits(self, player: int): - if can_farm_rupees(self, player): - return 999999999 - return sum(self.count(item_name, player) * amount for item_name, amount in self.rupees.items()) +def get_credits(state: CollectionState, player: int): + if can_farm_rupees(state, player): + return 999999999 + return state.prog_items["RUPEES", player] class LinksAwakeningRegion(Region): @@ -137,7 +128,7 @@ class GameStateAdapater: def get(self, item, default): if item == "RUPEES": - return self.state.get_credits(self.player) + return get_credits(self.state, self.player) elif item.endswith("_USED"): return 0 else: diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index b30919a82c..e45d9c0110 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,6 +1,5 @@ import binascii import bsdiff4 -import itertools import os import pkgutil import tempfile @@ -12,7 +11,7 @@ from worlds.AutoWorld import WebWorld, World from .Common import * from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData, ladxr_item_to_la_item_name, links_awakening_items, - links_awakening_items_by_name) + links_awakening_items_by_name, ItemName) from .LADXR import generator from .LADXR.itempool import ItemPool as LADXRItemPool from .LADXR.logic import Logic as LAXDRLogic @@ -29,6 +28,7 @@ from .Rom import LADXDeltaPatch DEVELOPER_MODE = False + class LinksAwakeningWebWorld(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", @@ -45,7 +45,7 @@ class LinksAwakeningWorld(World): After a previous adventure, Link is stranded on Koholint Island, full of mystery and familiar faces. Gather the 8 Instruments of the Sirens to wake the Wind Fish, so that Link can go home! """ - game: str = LINKS_AWAKENING # name of the game/world + game = LINKS_AWAKENING # name of the game/world web = LinksAwakeningWebWorld() option_definitions = links_awakening_options # options the player can set @@ -82,6 +82,14 @@ class LinksAwakeningWorld(World): player_options = None + rupees = { + ItemName.RUPEES_20: 0, + ItemName.RUPEES_50: 0, + ItemName.RUPEES_100: 100, + ItemName.RUPEES_200: 200, + ItemName.RUPEES_500: 500, + } + def convert_ap_options_to_ladxr_logic(self): self.player_options = { option: getattr(self.multiworld, option)[self.player] for option in self.option_definitions @@ -95,7 +103,6 @@ class LinksAwakeningWorld(World): self.ladxr_logic = LAXDRLogic(configuration_options=self.laxdr_options, world_setup=world_setup) self.ladxr_itempool = LADXRItemPool(self.ladxr_logic, self.laxdr_options, self.multiworld.random).toDict() - def create_regions(self) -> None: # Initialize self.convert_ap_options_to_ladxr_logic() @@ -401,9 +408,6 @@ class LinksAwakeningWorld(World): return "TRADING_ITEM_LETTER" - - - def generate_output(self, output_directory: str): # copy items back to locations for r in self.multiworld.get_regions(self.player): @@ -464,9 +468,8 @@ class LinksAwakeningWorld(World): bsdiff4.file_patch_inplace(out_path, title_patch.name) os.unlink(title_patch.name) - patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, - player_name=self.multiworld.player_name[self.player], patched_path=out_path) + player_name=self.multiworld.player_name[self.player], patched_path=out_path) patch.write() if not DEVELOPER_MODE: os.unlink(out_path) @@ -475,4 +478,20 @@ class LinksAwakeningWorld(World): return bytearray(self.multiworld.random.getrandbits(8) for _ in range(10)) + self.player.to_bytes(2, 'big') def modify_multidata(self, multidata: dict): - multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]] \ No newline at end of file + multidata["connect_names"][binascii.hexlify(self.multi_key).decode()] = multidata["connect_names"][self.multiworld.player_name[self.player]] + + def collect(self, state, item: Item) -> bool: + change = super().collect(state, item) + if change: + rupees = self.rupees.get(item.name, 0) + state.prog_items["RUPEES", item.player] += rupees + + return change + + def remove(self, state, item: Item) -> bool: + change = super().remove(state, item) + if change: + rupees = self.rupees.get(item.name, 0) + state.prog_items["RUPEES", item.player] -= rupees + + return change From a939f5048086a01fc3e329af01d01dddf55d474c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 22 Jun 2023 00:01:41 +0200 Subject: [PATCH 02/10] Clients: use certifi (#1879) * Clients: use certifi for wss On Windows, the local cert store might be outdated and refuse connection to some servers. * Clients: lazily create ssl_context --- CommonClient.py | 11 ++++++++++- requirements.txt | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index 87fa59cbf2..045cfba456 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -23,6 +23,7 @@ from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \ from Utils import Version, stream_input, async_start from worlds import network_data_package, AutoWorldRegister import os +import ssl if typing.TYPE_CHECKING: import kvui @@ -33,6 +34,12 @@ logger = logging.getLogger("Client") gui_enabled = not sys.stdout or "--nogui" not in sys.argv +@Utils.cache_argsless +def get_ssl_context(): + import certifi + return ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where()) + + class ClientCommandProcessor(CommandProcessor): def __init__(self, ctx: CommonContext): self.ctx = ctx @@ -589,7 +596,8 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) logger.info(f'Connecting to Archipelago server at {address}') try: - socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None) + socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None, + ssl=get_ssl_context() if address.startswith("wss://") else None) if ctx.ui is not None: ctx.ui.update_address_bar(server_url.netloc) ctx.server = Endpoint(socket) @@ -604,6 +612,7 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) except websockets.InvalidMessage: # probably encrypted if address.startswith("ws://"): + # try wss await server_loop(ctx, "ws" + address[1:]) else: ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage" diff --git a/requirements.txt b/requirements.txt index a082728f45..463a298d39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ schema>=0.7.5 kivy>=2.2.0 bsdiff4>=1.2.3 platformdirs>=3.5.1 +certifi>=2023.5.7 From f36468fc25ee7df9f054490fb98cb7de2b9c0c21 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 22 Jun 2023 08:51:02 +0200 Subject: [PATCH 03/10] Docs: add info about maintaining worlds (#1838) * Docs: add info about mainting worlds * Docs: fix typos in world maintainer * Docs: commit suggestions into world maintainers Thanks Joethepic and Silvris * Docs: fix more typos in world maintainer * Docs: more typos * Docs: world maintainers link to core maintainers * Docs: world maintainers voting on discord * Docs: add 'world maintainer' link to 'adding games' * Docs: unmaintained worlds in 'disabled' * Docs: world maintainer update from review Thanks LegendaryLinux * Doc: rephrase world maintainer voting --- docs/adding games.md | 1 + docs/contributing.md | 2 ++ docs/world maintainer.md | 60 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 docs/world maintainer.md diff --git a/docs/adding games.md b/docs/adding games.md index 1c59f256ae..24d9e499cd 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -341,3 +341,4 @@ The various methods and attributes are documented in `/worlds/AutoWorld.py[World [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md), though it is also recommended to look at existing implementations to see how all this works first-hand. Once you get all that, all that remains to do is test the game and publish your work. +Make sure to check out [world maintainer.md](./world%20maintainer.md) before publishing. diff --git a/docs/contributing.md b/docs/contributing.md index 4e90db95cd..899c06b922 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -10,3 +10,5 @@ Otherwise, we tend to judge code on a case to case basis. For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](/docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our [Discord](https://archipelago.gg/discord). +If you want to merge a new game, please make sure to read the responsibilities as +[world maintainer](/docs/world%20maintainer.md). diff --git a/docs/world maintainer.md b/docs/world maintainer.md new file mode 100644 index 0000000000..9cd7a378a2 --- /dev/null +++ b/docs/world maintainer.md @@ -0,0 +1,60 @@ +# World Maintainer + +A world maintainer is a person responsible for a world or part of a world in Archipelago. + +If a world author does not want to take on the responsibilities of a world maintainer, they can release their world as +an unofficial [APWorld](/docs/apworld%20specification.md) or maintain their own fork instead. + + +## Responsibilities + +Unless these are shared between multiple people, we expect the following from each world maintainer + +* Be on our Discord to get updates on problems with and suggestions for the world. +* Decide if a feature (pull request) should be merged. +* Review contents of such pull requests or organize peer reviews or post that you did not review the content. +* Fix or point out issues when core changes break your code. +* Use the watch function on GitHub, the #github-updates channel on Discord or check manually from time to time for new + pull requests. Core maintainers may also ping you if a pull request concerns your world. +* Test (or have tested) the world on the main branch from time to time, especially during RC (release candidate) phases + of development. +* Let us know of long unavailabilities. + + +## Becoming a World Maintainer + +### Adding a World + +When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you +nominate someone else (i.e. there are multiple devs). + +### Getting Voted + +When a world is unmaintained, the [core maintainers](https://github.com/orgs/ArchipelagoMW/people) +can vote for a new maintainer if there is a candidate. +For a vote to pass, the majority of participating core maintainers must vote in the affirmative. +The time limit is 1 week, but can end early if the majority is reached earlier. +Voting shall be conducted on Discord in #archipelago-dev. + + +## Dropping out + +### Resigning + +A world maintainer can resign. If no new maintainer steps up and gets voted, the world becomes unmaintained. + +### Getting Voted out + +A world maintainer can be voted out by the [core maintainers](https://github.com/orgs/ArchipelagoMW/people), +for example when they become unreachable. +For a vote to pass, the majority of participating core maintainers must vote in the affirmative. +The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and +made their case or was pinged and has been unreachable for more than 2 weeks already. +Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include +date, voting members and final result in the commit message. + + +## Handling of Unmaintained Worlds + +As long as worlds are known to work for the most part, they can stay included. Once a world becomes broken it shall be +moved from `worlds/` to `worlds_disabled/`. From abd8eaf36e2c88ab784d339dd61be5c4d1b28388 Mon Sep 17 00:00:00 2001 From: StripesOO7 <54711792+StripesOO7@users.noreply.github.com> Date: Fri, 23 Jun 2023 04:01:09 +0200 Subject: [PATCH 04/10] WebHost: Change default spoiler-option for games generated from WebHost to 3 instead of 0 (#1852) * Change default spoiler-option in WebHostLib/generate.py to 3 instead of 0 * shifting spoiler-default to the JS calls instead of setting it in generate.py --------- Co-authored-by: StripesOO7 <54711792+StripeesOO7@users.noreply.github.com> --- WebHostLib/static/assets/player-settings.js | 1 + WebHostLib/static/assets/weighted-settings.js | 1 + 2 files changed, 2 insertions(+) diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index 5d4aaffa9d..6707437fa3 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -364,6 +364,7 @@ const generateGame = (raceMode = false) => { weights: { player: settings }, presetData: { player: settings }, playerCount: 1, + spoiler: 3, race: raceMode ? '1' : '0', }).then((response) => { window.location.href = response.data.url; diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 11854d3ce0..6e86d470f0 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -1199,6 +1199,7 @@ const generateGame = (raceMode = false) => { weights: { player: JSON.stringify(settings) }, presetData: { player: JSON.stringify(settings) }, playerCount: 1, + spoiler: 3, race: raceMode ? '1' : '0', }).then((response) => { window.location.href = response.data.url; From 59ad9e97e5c77a78f8078361b42f8121af0c6bc4 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Thu, 22 Jun 2023 21:12:22 -0500 Subject: [PATCH 05/10] WebHost: Fix special-range value setting to `custom` when randomization is toggled off (#1856) * WebHost: Fix custom-range value setting to `custom` when randomization is toggled off * Remove redundant code * Add optional parameter default --- WebHostLib/static/assets/player-settings.js | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index 6707437fa3..f75ba90603 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -148,7 +148,7 @@ const buildOptionsTable = (settings, romOpts = false) => { randomButton.classList.add('randomize-button'); randomButton.setAttribute('data-key', setting); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); - randomButton.addEventListener('click', (event) => toggleRandomize(event, [select])); + randomButton.addEventListener('click', (event) => toggleRandomize(event, select)); if (currentSettings[gameName][setting] === 'random') { randomButton.classList.add('active'); select.disabled = true; @@ -185,7 +185,7 @@ const buildOptionsTable = (settings, romOpts = false) => { randomButton.classList.add('randomize-button'); randomButton.setAttribute('data-key', setting); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); - randomButton.addEventListener('click', (event) => toggleRandomize(event, [range])); + randomButton.addEventListener('click', (event) => toggleRandomize(event, range)); if (currentSettings[gameName][setting] === 'random') { randomButton.classList.add('active'); range.disabled = true; @@ -269,7 +269,7 @@ const buildOptionsTable = (settings, romOpts = false) => { randomButton.setAttribute('data-key', setting); randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!'); randomButton.addEventListener('click', (event) => toggleRandomize( - event, [specialRange, specialRangeSelect]) + event, specialRange, specialRangeSelect) ); if (currentSettings[gameName][setting] === 'random') { randomButton.classList.add('active'); @@ -294,23 +294,25 @@ const buildOptionsTable = (settings, romOpts = false) => { return table; }; -const toggleRandomize = (event, inputElements) => { +const toggleRandomize = (event, inputElement, optionalSelectElement = null) => { const active = event.target.classList.contains('active'); const randomButton = event.target; if (active) { randomButton.classList.remove('active'); - for (const element of inputElements) { - element.disabled = undefined; - updateGameSetting(element); + inputElement.disabled = undefined; + if (optionalSelectElement) { + optionalSelectElement.disabled = undefined; } } else { randomButton.classList.add('active'); - for (const element of inputElements) { - element.disabled = true; - updateGameSetting(randomButton); + inputElement.disabled = true; + if (optionalSelectElement) { + optionalSelectElement.disabled = true; } } + + updateGameSetting(randomButton); }; const updateBaseSetting = (event) => { From f3c788d0cc3a462af95a8093a37f406624bc1dcd Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 21 Jun 2023 05:18:54 +0200 Subject: [PATCH 06/10] Witness: Fix missing location All Pressure Plates puzzles are now always locations. This makes a line where a Pressure Plates location gets added and a different one cause incorrect behavior. --- worlds/witness/locations.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 9618046ce3..1aa186e70d 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -463,10 +463,6 @@ class WitnessPlayerLocations: self.PANEL_TYPES_TO_SHUFFLE = {"General", "Laser"} self.CHECK_LOCATIONS = StaticWitnessLocations.GENERAL_LOCATIONS.copy() - if get_option_value(world, player, "puzzle_randomization") == 1: - self.CHECK_LOCATIONS.remove("Keep Pressure Plates 4") - self.CHECK_LOCATIONS.add("Keep Pressure Plates 2") - doors = get_option_value(world, player, "shuffle_doors") >= 2 earlyutm = is_option_enabled(world, player, "early_secret_area") victory = get_option_value(world, player, "victory_condition") From 78b981228ae3ff83258e4f9df2b30cba84786cbc Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 23 Jun 2023 10:17:35 +0200 Subject: [PATCH 07/10] Generate: improve error message for missing game (#1857) --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 52 +++++++++++++++++++++++---------------------- WebHostLib/check.py | 2 +- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Generate.py b/Generate.py index d98edc3f58..ab8eab22d6 100644 --- a/Generate.py +++ b/Generate.py @@ -447,6 +447,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) + if ret.game not in AutoWorldRegister.world_types: + picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0] + raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? " + f"Check your spelling or installation of that world.") + if ret.game not in weights: raise Exception(f"No game options for selected game \"{ret.game}\" found.") @@ -461,32 +466,29 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b for option_key, option in Options.common_options.items(): setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default))) - if ret.game in AutoWorldRegister.world_types: - for option_key, option in world_type.option_definitions.items(): + for option_key, option in world_type.option_definitions.items(): + handle_option(ret, game_weights, option_key, option, plando_options) + for option_key, option in Options.per_game_common_options.items(): + # skip setting this option if already set from common_options, defaulting to root option + if option_key not in world_type.option_definitions and \ + (option_key not in Options.common_options or option_key in game_weights): handle_option(ret, game_weights, option_key, option, plando_options) - for option_key, option in Options.per_game_common_options.items(): - # skip setting this option if already set from common_options, defaulting to root option - if option_key not in world_type.option_definitions and \ - (option_key not in Options.common_options or option_key in game_weights): - handle_option(ret, game_weights, option_key, option, plando_options) - if PlandoOptions.items in plando_options: - ret.plando_items = game_weights.get("plando_items", []) - if ret.game == "Minecraft" or ret.game == "Ocarina of Time": - # bad hardcoded behavior to make this work for now - ret.plando_connections = [] - if PlandoOptions.connections in plando_options: - options = game_weights.get("plando_connections", []) - for placement in options: - if roll_percentage(get_choice("percentage", placement, 100)): - ret.plando_connections.append(PlandoConnection( - get_choice("entrance", placement), - get_choice("exit", placement), - get_choice("direction", placement) - )) - elif ret.game == "A Link to the Past": - roll_alttp_settings(ret, game_weights, plando_options) - else: - raise Exception(f"Unsupported game {ret.game}") + if PlandoOptions.items in plando_options: + ret.plando_items = game_weights.get("plando_items", []) + if ret.game == "Minecraft" or ret.game == "Ocarina of Time": + # bad hardcoded behavior to make this work for now + ret.plando_connections = [] + if PlandoOptions.connections in plando_options: + options = game_weights.get("plando_connections", []) + for placement in options: + if roll_percentage(get_choice("percentage", placement, 100)): + ret.plando_connections.append(PlandoConnection( + get_choice("entrance", placement), + get_choice("exit", placement), + get_choice("direction", placement) + )) + elif ret.game == "A Link to the Past": + roll_alttp_settings(ret, game_weights, plando_options) return ret diff --git a/WebHostLib/check.py b/WebHostLib/check.py index 3daef72c03..0c1e090dbe 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -92,7 +92,7 @@ def roll_options(options: Dict[str, Union[dict, str]], rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data, plando_options=plando_options) except Exception as e: - results[filename] = f"Failed to generate mystery in {filename}: {e}" + results[filename] = f"Failed to generate options in {filename}: {e}" else: results[filename] = True return results, rolled_results From ec0822c5eba716a22393304e01a1409fb31f51f1 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sat, 24 Jun 2023 12:59:14 +0200 Subject: [PATCH 08/10] Docs: Mention Git in the "Optional" section of "Running from Source" (#1880) * Docs: Mention Git in the "Optional" section of "Running from Source" GIt is required to install the Zilliandomizer package. Also, this is probably just nice to have. * Remove mention of Zillion so the text doesn't need updating. * Update docs/running from source.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update docs/running from source.md Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Mention PyCharm's git integration --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- docs/running from source.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/running from source.md b/docs/running from source.md index 611ac04e1b..c0f4bf5802 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -69,6 +69,19 @@ It should be dropped as "SNI" into the root folder of the project. Alternatively host.yaml at your SNI folder. +## Optional: Git + +[Git](https://git-scm.com) is required to install some of the packages that Archipelago depends on. +It may be possible to run Archipelago from source without it, at your own risk. + +It is also generally recommended to have Git installed and understand how to use it, especially if you're thinking about contributing. + +You can download the latest release of Git at [The downloads page on the Git website](https://git-scm.com/downloads). + +Beyond that, there are also graphical interfaces for Git that make it more accessible. +For repositories on Github (such as this one), [Github Desktop](https://desktop.github.com) is one such option. +PyCharm has a built-in version control integration that supports Git. + ## Running tests Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. From f1ccf1b663a3b9cc74e95ca8400d6d29f81c679a Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 24 Jun 2023 18:02:50 -0500 Subject: [PATCH 09/10] reenable ping --- WebHostLib/customserver.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index ebde8cc703..8fbf692dec 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -169,13 +169,11 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, ctx.init_save() ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None try: - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None, - ping_interval=None, ssl=ssl_context) + ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context) await ctx.server except Exception: # likely port in use - in windows this is OSError, but I didn't check the others - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None, - ping_interval=None, ssl=ssl_context) + ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context) await ctx.server port = 0 From 7a4e903906ccd64b655e7836d3bccf5747976896 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Sat, 24 Jun 2023 19:58:54 -0400 Subject: [PATCH 10/10] TLOZ: APworld support (#1884) - Remove a relative import in Rules.py - Clean up a few unused imports in __init__.py - Use pkgutil instead of open when applying base patch - make sure rom_name is initialized correctly in modify_multidata * use os.path.join() instead of explicit "/" --- worlds/tloz/Rules.py | 2 +- worlds/tloz/__init__.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py index 1e66e5a849..12bf466bce 100644 --- a/worlds/tloz/Rules.py +++ b/worlds/tloz/Rules.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from ..generic.Rules import add_rule +from worlds.generic.Rules import add_rule from .Locations import food_locations, shop_locations from .ItemPool import dangerous_weapon_locations from .Options import StartingPosition diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 356f9e5ff5..2af92a7997 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -1,8 +1,7 @@ -import logging import os import threading -import pkgutil -from typing import NamedTuple, Union, Dict, Any +from pkgutil import get_data +from typing import Dict, Any import bsdiff4 @@ -168,9 +167,8 @@ class TLoZWorld(World): # Remove map/compass check so they're always on # Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to # go past 0x1F items for dungeon items. - base_patch_location = os.path.dirname(__file__) + "/z1_base_patch.bsdiff4" - with open(base_patch_location, "rb") as base_patch: - rom_data = bsdiff4.patch(rom.read(), base_patch.read()) + base_patch = get_data(__name__, os.path.join(os.path.dirname(__file__), "z1_base_patch.bsdiff4")) + rom_data = bsdiff4.patch(rom.read(), base_patch) rom_data = bytearray(rom_data) # Set every item to the new nothing value, but keep room flags. Type 2 boss roars should # become type 1 boss roars, so we at least keep the sound of roaring where it should be. @@ -275,8 +273,10 @@ class TLoZWorld(World): def modify_multidata(self, multidata: dict): import base64 self.rom_name_available_event.wait() - new_name = base64.b64encode(bytes(self.rom_name)).decode() - multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + rom_name = getattr(self, "rom_name", None) + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] def get_filler_item_name(self) -> str: if self.filler_items is None: @@ -320,4 +320,4 @@ class TLoZItem(Item): class TLoZLocation(Location): - game = 'The Legend of Zelda' \ No newline at end of file + game = 'The Legend of Zelda'