From 04b6c310768a58d75e33c90e5eb1e5fe3d167ec6 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 21 Oct 2022 23:26:40 +0200 Subject: [PATCH] SoE: update to v042 and balancing changes (#1125) * SoE: rebalancing and cleanup * ModuleUpdate: make url detection more generic * SoE: change item rules to depend on target player difficulty * SoE: Update to pyevermizer 0.41.0 * adds footknight * adds location difficulty * SoE: minor optimization in item rule if .. in is faster with sets * SoE: drop support of patch format v3 * SoE: fix some typing and warnings * SoE: cleanup imports --- ModuleUpdate.py | 31 ++++++----- worlds/soe/Logic.py | 22 +++++--- worlds/soe/Options.py | 29 ++++++++-- worlds/soe/Patch.py | 31 ++--------- worlds/soe/__init__.py | 102 ++++++++++++++++++++++++++++-------- worlds/soe/requirements.txt | 28 +++++----- 6 files changed, 157 insertions(+), 86 deletions(-) diff --git a/ModuleUpdate.py b/ModuleUpdate.py index 0768b376..2b6aed1f 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -39,18 +39,25 @@ def update(yes=False, force=False): path = os.path.join(os.path.dirname(__file__), req_file) with open(path) as requirementsfile: for line in requirementsfile: - if line.startswith('https://'): - # extract name and version from url - wheel = line.split('/')[-1] - name, version, _ = wheel.split('-', 2) - line = f'{name}=={version}' - elif line.startswith('git+https://'): - # extract name and version - end = line.split('/')[-1] - name_hash, egg = end.split("#", 1) - name, _ = name_hash.split("@", 1) - version = egg.split('==')[-1] - line = f'{name}=={version}' + if line.startswith(("https://", "git+https://")): + # extract name and version for url + rest = line.split('/')[-1] + line = "" + if "#egg=" in rest: + # from egg info + rest, egg = rest.split("#egg=", 1) + egg = egg.split(";", 1)[0] + if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")): + line = egg + else: + egg = "" + if "@" in rest and not line: + raise ValueError("Can't deduce version from requirement") + elif not line: + # from filename + rest = rest.replace(".zip", "-").replace(".tar.gz", "-") + name, version, _ = rest.split("-", 2) + line = f'{egg or name}=={version}' requirements = pkg_resources.parse_requirements(line) for requirement in requirements: requirement = str(requirement) diff --git a/worlds/soe/Logic.py b/worlds/soe/Logic.py index 97c73a1b..3c173dec 100644 --- a/worlds/soe/Logic.py +++ b/worlds/soe/Logic.py @@ -1,10 +1,11 @@ -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin -from .Options import EnergyCore -from typing import Set -# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? +from typing import Protocol, Set +from BaseClasses import MultiWorld +from worlds.AutoWorld import LogicMixin from . import pyevermizer +from .Options import EnergyCore + +# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early? # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) @@ -15,9 +16,16 @@ items = [item for item in filter(lambda item: item.progression, pyevermizer.get_ if item.name not in item_names and not item_names.add(item.name)] +class LogicProtocol(Protocol): + def has(self, name: str, player: int) -> bool: ... + def item_count(self, name: str, player: int) -> int: ... + def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ... + def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ... + + # when this module is loaded, this mixin will extend BaseClasses.CollectionState class SecretOfEvermoreLogic(LogicMixin): - def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: + def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int: """ Returns reached count of one of evermizer's progress steps based on collected items. i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP @@ -44,7 +52,7 @@ class SecretOfEvermoreLogic(LogicMixin): return n return n - def soe_has(self, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: + def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool: """ Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE """ diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index c718cb4a..f1a30745 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -1,18 +1,33 @@ import typing -from Options import Option, Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing + +from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing +# typing boilerplate +class FlagsProtocol(typing.Protocol): + value: int + default: int + flags: typing.List[str] + + +class FlagProtocol(typing.Protocol): + value: int + default: int + flag: str + + +# meta options class EvermizerFlags: flags: typing.List[str] - def to_flag(self) -> str: + def to_flag(self: FlagsProtocol) -> str: return self.flags[self.value] class EvermizerFlag: flag: str - def to_flag(self) -> str: + def to_flag(self: FlagProtocol) -> str: return self.flag if self.value != self.default else '' @@ -23,6 +38,7 @@ class OffOnFullChoice(Choice): alias_chaos = 2 +# actual options class Difficulty(EvermizerFlags, Choice): """Changes relative spell cost and stuff""" display_name = "Difficulty" @@ -168,6 +184,7 @@ class TrapCount(Range): default = 0 +# more meta options class ItemChanceMeta(AssembleOptions): def __new__(mcs, name, bases, attrs): if 'item_name' in attrs: @@ -183,6 +200,7 @@ class TrapChance(Range, metaclass=ItemChanceMeta): default = 20 +# more actual options class TrapChanceQuake(TrapChance): """Sets the chance/ratio of quake traps""" item_name = "Quake Trap" @@ -210,11 +228,12 @@ class TrapChanceOHKO(TrapChance): class SoEProgressionBalancing(ProgressionBalancing): default = 30 - __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") + __doc__ = ProgressionBalancing.__doc__.replace(f"default {ProgressionBalancing.default}", f"default {default}") \ + if ProgressionBalancing.__doc__ else None special_range_names = {**ProgressionBalancing.special_range_names, "normal": default} -soe_options: typing.Dict[str, type(Option)] = { +soe_options: typing.Dict[str, AssembleOptions] = { "difficulty": Difficulty, "energy_core": EnergyCore, "required_fragments": RequiredFragments, diff --git a/worlds/soe/Patch.py b/worlds/soe/Patch.py index f6a0a69f..f4de5d06 100644 --- a/worlds/soe/Patch.py +++ b/worlds/soe/Patch.py @@ -1,9 +1,8 @@ -import bsdiff4 -import yaml +import os from typing import Optional + import Utils from worlds.Files import APDeltaPatch -import os USHASH = '6e9c94511d04fac6e0a1e582c170be3a' @@ -24,6 +23,8 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str: options = Utils.get_options() if not file_name: file_name = options["soe_options"]["rom_file"] + if not file_name: + raise ValueError("Missing soe_options -> rom_file from host.yaml") if not os.path.exists(file_name): file_name = Utils.user_path(file_name) return file_name @@ -37,30 +38,6 @@ def read_rom(stream, strip_header=True) -> bytes: return data -def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: - """Generate old (<4) apbp format yaml""" - patch = yaml.dump({"meta": metadata, - "patch": patch, - "game": "Secret of Evermore", - # minimum version of patch system expected for patching to be successful - "compatible_version": 1, - "version": 2, - "base_checksum": USHASH}) - return patch.encode(encoding="utf-8-sig") - - -def generate_patch(vanilla_file, randomized_file, metadata: Optional[dict] = None) -> bytes: - """Generate old (<4) apbp format patch data. Run through lzma to get a complete apbp file.""" - with open(vanilla_file, "rb") as f: - vanilla = read_rom(f) - with open(randomized_file, "rb") as f: - randomized = read_rom(f) - if metadata is None: - metadata = {} - patch = bsdiff4.diff(vanilla, randomized) - return generate_yaml(patch, metadata) - - if __name__ == '__main__': import sys print('Please use ../../Patch.py', file=sys.stderr) diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index 4885fd31..007bc6dc 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -1,12 +1,12 @@ -from ..AutoWorld import World, WebWorld -from ..generic.Rules import set_rule -from BaseClasses import Region, Location, Entrance, Item, RegionType, Tutorial, ItemClassification -from Utils import output_path -import typing +import itertools import os import os.path import threading -import itertools +import typing +from worlds.AutoWorld import WebWorld, World +from worlds.generic.Rules import add_item_rule, set_rule +from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, RegionType, Tutorial +from Utils import output_path try: import pyevermizer # from package @@ -16,7 +16,7 @@ except ImportError: from . import pyevermizer # as part of the source tree from . import Logic # load logic mixin -from .Options import soe_options, EnergyCore, RequiredFragments, AvailableFragments +from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments from .Patch import SoEDeltaPatch, get_base_rom_path """ @@ -154,9 +154,9 @@ class SoEWorld(World): option_definitions = soe_options topology_present = False remote_items = False - data_version = 3 + data_version = 4 web = SoEWebWorld() - required_client_version = (0, 3, 3) + required_client_version = (0, 3, 5) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -188,7 +188,7 @@ class SoEWorld(World): return SoEItem(event, ItemClassification.progression, None, self.player) def create_item(self, item: typing.Union[pyevermizer.Item, str]) -> Item: - if type(item) is str: + if isinstance(item, str): item = self.item_id_to_raw[self.item_name_to_id[item]] if item.type == pyevermizer.CHECK_TRAP: classification = ItemClassification.trap @@ -208,14 +208,68 @@ class SoEWorld(World): raise FileNotFoundError(rom_file) def create_regions(self): + # exclude 'hidden' on easy + max_difficulty = 1 if self.world.difficulty[self.player] == Difficulty.option_easy else 256 + # TODO: generate *some* regions from locations' requirements? r = Region('Menu', RegionType.Generic, 'Menu', self.player, self.world) r.exits = [Entrance(self.player, 'New Game', r)] self.world.regions += [r] + # group locations into spheres (1, 2, 3+ at index 0, 1, 2) + spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {} + for loc in _locations: + spheres.setdefault(min(2, len(loc.requires)), {}).setdefault(loc.type, []).append( + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r, + loc.difficulty > max_difficulty)) + + # location balancing data + trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = { + 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1 + 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2 + } + + # mark some as excluded based on numbers above + for trash_sphere, fills in trash_fills.items(): + for typ, counts in fills.items(): + count = counts[self.world.difficulty[self.player].value] + for location in self.world.random.sample(spheres[trash_sphere][typ], count): + location.progress_type = LocationProgressType.EXCLUDED + # TODO: do we need to set an item rule? + + def sphere1_blocked_items_rule(item): + if isinstance(item, SoEItem): + # disable certain items in sphere 1 + if item.name in {"Gauge", "Wheel"}: + return False + # and some more for non-easy, non-mystery + if self.world.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery): + if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}: + return False + return True + + for locations in spheres[0].values(): + for location in locations: + add_item_rule(location, sphere1_blocked_items_rule) + + # make some logically late(r) bosses priority locations to increase complexity + if self.world.difficulty[self.player] == Difficulty.option_mystery: + late_count = self.world.random.randint(0, 2) + else: + late_count = self.world.difficulty[self.player].value + late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala", + "Mungola", "Lightning Storm", "Magmar", "Volcano Viper") + late_locations = self.world.random.sample(late_bosses, late_count) + + # add locations to the world r = Region('Ingame', RegionType.Generic, 'Ingame', self.player, self.world) - r.locations = [SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r) - for loc in _locations] + for sphere in spheres.values(): + for locations in sphere.values(): + for location in locations: + r.locations.append(location) + if location.name in late_locations: + location.progress_type = LocationProgressType.PRIORITY + r.locations.append(SoELocation(self.player, 'Done', None, r)) self.world.regions += [r] @@ -269,6 +323,7 @@ class SoEWorld(World): if v < c: return self.create_item(trap_names[t]) v -= c + assert False, "Bug in create_trap" for _ in range(trap_count): if len(ingredients) < 1: @@ -289,7 +344,7 @@ class SoEWorld(World): location = self.world.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) - def make_rule(self, requires: typing.List[typing.Tuple[int]]) -> typing.Callable[[typing.Any], bool]: + def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]: def rule(state) -> bool: for count, progress in requires: if not state.soe_has(progress, self.world, self.player, count): @@ -321,8 +376,8 @@ class SoEWorld(World): while len(self.connect_name.encode('utf-8')) > 32: self.connect_name = self.connect_name[:-1] self.connect_name_available_event.set() - placement_file = None - out_file = None + placement_file = "" + out_file = "" try: money = self.world.money_modifier[self.player].value exp = self.world.exp_modifier[self.player].value @@ -346,14 +401,15 @@ class SoEWorld(World): with open(placement_file, "wb") as f: # generate placement file for location in filter(lambda l: l.player == self.player, self.world.get_locations()): item = location.item - if item.code is None: + assert item is not None, "Can't handle unfilled location" + if item.code is None or location.address is None: continue # skip events loc = self.location_id_to_raw[location.address] if item.player != self.player: line = f'{loc.type},{loc.index}:{pyevermizer.CHECK_NONE},{item.code},{item.player}\n' else: - item = self.item_id_to_raw[item.code] - line = f'{loc.type},{loc.index}:{item.type},{item.index}\n' + soe_item = self.item_id_to_raw[item.code] + line = f'{loc.type},{loc.index}:{soe_item.type},{soe_item.index}\n' f.write(line.encode('utf-8')) if not os.path.exists(rom_file): @@ -364,14 +420,14 @@ class SoEWorld(World): patch = SoEDeltaPatch(patch_file, player=self.player, player_name=player_name, patched_path=out_file) patch.write() - except: + except Exception: raise finally: try: os.unlink(placement_file) os.unlink(out_file) os.unlink(out_file[:-4] + '_SPOILER.log') - except: + except FileNotFoundError: pass def modify_multidata(self, multidata: dict): @@ -388,11 +444,15 @@ class SoEWorld(World): class SoEItem(Item): game: str = "Secret of Evermore" + __slots__ = () # disable __dict__ class SoELocation(Location): game: str = "Secret of Evermore" + __slots__ = () # disables __dict__ once Location has __slots__ - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): + def __init__(self, player: int, name: str, address: typing.Optional[int], parent: Region, exclude: bool = False): super().__init__(player, name, address, parent) + # unconditional assignments favor a split dict, saving memory + self.progress_type = LocationProgressType.EXCLUDED if exclude else LocationProgressType.DEFAULT self.event = not address diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 7f6a11e4..54eae8f1 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,14 +1,14 @@ -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-win_amd64.whl#egg=pyevermizer; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.8' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.9' -https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer; sys_platform == 'darwin' and python_version == '3.10' -#https://github.com/black-sliver/pyevermizer/releases/download/v0.41.3/pyevermizer-0.41.3.tar.gz#egg=pyevermizer; python_version == '3.11' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-win_amd64.whl#egg=pyevermizer==0.42.0; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#egg=pyevermizer==0.42.0; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp38-cp38-macosx_10_9_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.8' +#https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-macosx_10_9_x86_64.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp39-cp39-macosx_10_9_universal2.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.9' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0-cp310-cp310-macosx_10_9_universal2.whl#egg=pyevermizer==0.42.0; sys_platform == 'darwin' and python_version == '3.10' +https://github.com/black-sliver/pyevermizer/releases/download/v0.42.0/pyevermizer-0.42.0.tar.gz#egg=pyevermizer==0.42.0; python_version < '3.8' or python_version > '3.10' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64')