From c80636646964fad23385f6d01b9105a04464a48d Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 14 Jul 2022 03:37:45 -0400 Subject: [PATCH 1/4] Sm comeback too strict (#755) --- worlds/sm/__init__.py | 11 ++++++----- worlds/sm/variaRandomizer/graph/graph.py | 16 ++++++++++++++++ worlds/sm/variaRandomizer/randomizer.py | 2 ++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 29d428abc2..fe1323caec 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -7,6 +7,8 @@ import threading import base64 from typing import Set, List, TextIO +from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils + logger = logging.getLogger("Super Metroid") from .Locations import lookup_name_to_id as locations_lookup_name_to_id @@ -654,11 +656,10 @@ class SMLocation(Location): def can_comeback(self, state: CollectionState, item: Item): randoExec = state.world.worlds[self.player].variaRando.randoExec for key in locationsDict[self.name].AccessFrom.keys(): - if (randoExec.areaGraph.canAccess( state.smbm[self.player], - key, - randoExec.graphSettings.startAP, - state.smbm[self.player].maxDiff, - None)): + if (randoExec.areaGraph.canAccessList( state.smbm[self.player], + key, + [randoExec.graphSettings.startAP, 'Landing Site'] if not GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else ['Landing Site'], + state.smbm[self.player].maxDiff)): return True return False diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index bcbf138123..6ca7465a7e 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -367,6 +367,22 @@ class AccessGraph(object): #print("canAccess: {}".format(can)) return can + # test access from an access point to a list of others, given an optional item + def canAccessList(self, smbm, srcAccessPointName, destAccessPointNameList, maxDiff, item=None): + if item is not None: + smbm.addItem(item) + #print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName)) + destAccessPointList = [self.accessPoints[destAccessPointName] for destAccessPointName in destAccessPointNameList] + srcAccessPoint = self.accessPoints[srcAccessPointName] + availAccessPoints = self.getAvailableAccessPoints(srcAccessPoint, smbm, maxDiff, item) + can = any(ap in availAccessPoints for ap in destAccessPointList) + # if not can: + # self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()])) + if item is not None: + smbm.removeItem(item) + #print("canAccess: {}".format(can)) + return can + # returns a list of AccessPoint instances from srcAccessPointName to destAccessPointName # (not including source ap) # or None if no possible path diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 0da2b2d042..ebb87c520b 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -341,6 +341,8 @@ class VariaRandomizer: if preset == 'custom': PresetLoader.factory(world.custom_preset[player].value).load(self.player) elif preset == 'varia_custom': + if len(world.varia_custom_preset[player].value) == 0: + raise Exception("varia_custom was chosen but varia_custom_preset is missing.") url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService' preset_name = next(iter(world.varia_custom_preset[player].value)) payload = '{{"preset": "{}"}}'.format(preset_name) From 122590fc6873340ce9a6e0530dbc0014ee6d96e3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 14 Jul 2022 02:39:53 -0500 Subject: [PATCH 2/4] lttp: move open pyramid to new options system (#762) --- BaseClasses.py | 3 --- Generate.py | 3 --- Main.py | 1 - worlds/alttp/Options.py | 31 +++++++++++++++++++++++++++++++ worlds/alttp/Rom.py | 2 +- worlds/alttp/__init__.py | 11 ----------- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index a186404727..796db59219 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -126,7 +126,6 @@ class MultiWorld(): set_player_attr('beemizer_total_chance', 0) set_player_attr('beemizer_trap_chance', 0) set_player_attr('escape_assist', []) - set_player_attr('open_pyramid', False) set_player_attr('treasure_hunt_icon', 'Triforce Piece') set_player_attr('treasure_hunt_count', 0) set_player_attr('clock_mode', False) @@ -1431,8 +1430,6 @@ class Spoiler(): outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player]) if self.world.shuffle[player] != "vanilla": outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed) - outfile.write('Pyramid hole pre-opened: %s\n' % ( - 'Yes' if self.world.open_pyramid[player] else 'No')) outfile.write('Shop inventory shuffle: %s\n' % bool_to_text("i" in self.world.shop_shuffle[player])) outfile.write('Shop price shuffle: %s\n' % diff --git a/Generate.py b/Generate.py index b46c730c9a..125fab4163 100644 --- a/Generate.py +++ b/Generate.py @@ -583,9 +583,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.goal = goals[goal] - # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when - # fast ganon + ganon at hole - ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal') extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available') diff --git a/Main.py b/Main.py index acbb4ad5cf..6daa16d908 100644 --- a/Main.py +++ b/Main.py @@ -47,7 +47,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.item_functionality = args.item_functionality.copy() world.timer = args.timer.copy() world.goal = args.goal.copy() - world.open_pyramid = args.open_pyramid.copy() world.boss_shuffle = args.shufflebosses.copy() world.enemy_health = args.enemy_health.copy() world.enemy_damage = args.enemy_damage.copy() diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index d7f9becbfd..b42a5eb377 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,5 +1,6 @@ import typing +from BaseClasses import MultiWorld from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink @@ -27,6 +28,35 @@ class Goal(Choice): option_hand_in = 2 +class OpenPyramid(Choice): + """Determines whether the hole at the top of pyramid is open. + Goal will open the pyramid if the goal requires you to kill Ganon, without needing to kill Agahnim 2. + Auto is the same as goal except if Ganon's dropdown is in another location, the hole will be closed.""" + display_name = "Open Pyramid Hole" + option_closed = 0 + option_open = 1 + option_goal = 2 + option_auto = 3 + default = option_goal + + alias_true = option_open + alias_false = option_closed + alias_yes = option_open + alias_no = option_closed + + def to_bool(self, world: MultiWorld, player: int) -> bool: + if self.value == self.option_goal: + return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} + elif self.value == self.option_auto: + return world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} \ + and (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not + world.shuffle_ganon) + elif self.value == self.option_open: + return True + else: + return False + + class DungeonItem(Choice): value: int option_original_dungeon = 0 @@ -331,6 +361,7 @@ class AllowCollect(Toggle): alttp_options: typing.Dict[str, type(Option)] = { "crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_ganon": CrystalsGanon, + "open_pyramid": OpenPyramid, "bigkey_shuffle": bigkey_shuffle, "smallkey_shuffle": smallkey_shuffle, "compass_shuffle": compass_shuffle, diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 72cd1ceac5..c16bbf5322 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1247,7 +1247,7 @@ def patch_rom(world, rom, player, enemized): rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest rom.write_byte(0x50599, 0x00) # disable below ganon chest rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest - rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player] else 0x00) # pre-open Pyramid Hole + rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[ player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0 rom.write_byte(0xF5D73, 0xF0) # bees are catchable diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8e4ec1c143..871c44684d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -176,17 +176,6 @@ class ALTTPWorld(World): def create_regions(self): player = self.player world = self.world - if world.open_pyramid[player] == 'goal': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', - 'localganontriforcehunt', 'ganonpedestal'} - elif world.open_pyramid[player] == 'auto': - world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', - 'localganontriforcehunt', 'ganonpedestal'} and \ - (world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', - 'dungeonscrossed'} or not world.shuffle_ganon) - else: - world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get( - world.open_pyramid[player], 'auto') world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player]) From 6e0a0c5c4ae33b1a860d9a8a2b9d696ff9ebb2af Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 14 Jul 2022 09:46:03 +0200 Subject: [PATCH 3/4] Core: skip second sanity check when pushing an item into a location (-O) (#745) --- BaseClasses.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 796db59219..6816617279 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -389,20 +389,14 @@ class MultiWorld(): self.state.collect(item, True) def push_item(self, location: Location, item: Item, collect: bool = True): - if not isinstance(location, Location): - raise RuntimeError( - 'Cannot assign item %s to invalid location %s (player %d).' % (item, location, item.player)) + assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}." + location.item = item + item.location = location + item.world = self # try to not have this here anymore and create it with item? + if collect: + self.state.collect(item, location.event, location) - if location.can_fill(self.state, item, False): - location.item = item - item.location = location - item.world = self # try to not have this here anymore - if collect: - self.state.collect(item, location.event, location) - - logging.debug('Placed %s at %s', item, location) - else: - raise RuntimeError('Cannot assign item %s to location %s.' % (item, location)) + logging.debug('Placed %s at %s', item, location) def get_entrances(self) -> List[Entrance]: if self._cached_entrances is None: From e804f592debb7e0942bdee34a2e591eacd7ecbe5 Mon Sep 17 00:00:00 2001 From: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Date: Thu, 14 Jul 2022 03:51:00 -0400 Subject: [PATCH 4/4] SC2: Windows ".dll missing" fix and fix for finding SC2 install automatically (#721) --- Starcraft2Client.py | 124 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index f9b6b43fe3..e9e06335ac 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -19,7 +19,13 @@ from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET from worlds.sc2wol import SC2WoLWorld -from Utils import init_logging +from pathlib import Path +import re +from MultiServer import mark_raw +import ctypes +import sys + +from Utils import init_logging, is_windows if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -73,6 +79,17 @@ class StarcraftClientProcessor(ClientCommandProcessor): request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx) return True + @mark_raw + def _cmd_set_path(self, path: str = '') -> bool: + """Manually set the SC2 install directory (if the automatic detection fails).""" + if path: + os.environ["SC2PATH"] = path + check_mod_install() + return True + else: + sc2_logger.warning("When using set_path, you must type the path to your SC2 install directory.") + return False + class SC2Context(CommonContext): command_processor = StarcraftClientProcessor @@ -111,6 +128,11 @@ class SC2Context(CommonContext): for mission in slot_req_table: self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) + # Look for and set SC2PATH. + # check_game_install_path() returns True if and only if it finds + sets SC2PATH. + if "SC2PATH" not in os.environ and check_game_install_path(): + check_mod_install() + if cmd in {"PrintJSON"}: if "receiving" in args: if self.slot_concerns_self(args["receiving"]): @@ -415,8 +437,9 @@ async def starcraft_launch(ctx: SC2Context, mission_id): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") - run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), - name="Archipelago", fullscreen=True)], realtime=True) + with DllDirectory(None): + run_game(sc2.maps.get(maps_table[mission_id - 1]), [Bot(Race.Terran, ArchipelagoBot(ctx, mission_id), + name="Archipelago", fullscreen=True)], realtime=True) class ArchipelagoBot(sc2.bot_ai.BotAI): @@ -796,6 +819,101 @@ def initialize_blank_mission_dict(location_table): return unlocks +def check_game_install_path() -> bool: + # First thing: go to the default location for ExecuteInfo. + # An exception for Windows is included because it's very difficult to find ~\Documents if the user moved it. + if is_windows: + # The next five lines of utterly inscrutable code are brought to you by copy-paste from Stack Overflow. + # https://stackoverflow.com/questions/6227590/finding-the-users-my-documents-path/30924555# + import ctypes.wintypes + CSIDL_PERSONAL = 5 # My Documents + SHGFP_TYPE_CURRENT = 0 # Get current, not default value + + buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) + ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) + documentspath = buf.value + einfo = str(documentspath / Path("StarCraft II\\ExecuteInfo.txt")) + else: + einfo = str(sc2.paths.get_home() / Path(sc2.paths.USERPATH[sc2.paths.PF])) + + # Check if the file exists. + if os.path.isfile(einfo): + + # Open the file and read it, picking out the latest executable's path. + with open(einfo) as f: + content = f.read() + if content: + base = re.search(r" = (.*)Versions", content).group(1) + if os.path.exists(base): + executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions") + + # Finally, check the path for an actual executable. + # If we find one, great. Set up the SC2PATH. + if os.path.isfile(executable): + sc2_logger.info(f"Found an SC2 install at {base}!") + sc2_logger.debug(f"Latest executable at {executable}.") + os.environ["SC2PATH"] = base + sc2_logger.debug(f"SC2PATH set to {base}.") + return True + else: + sc2_logger.warning(f"We may have found an SC2 install at {base}, but couldn't find {executable}.") + else: + sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") + else: + sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.") + return False + + +def check_mod_install() -> bool: + # Pull up the SC2PATH if set. If not, encourage the user to manually run /set_path. + try: + # Check inside the Mods folder for Archipelago.SC2Mod. If found, tell user. If not, tell user. + if os.path.isfile(modfile := (os.environ["SC2PATH"] / Path("Mods") / Path("Archipelago.SC2Mod"))): + sc2_logger.info(f"Archipelago mod found at {modfile}.") + return True + else: + sc2_logger.warning(f"Archipelago mod could not be found at {modfile}. Please install the mod file there.") + except KeyError: + sc2_logger.warning(f"SC2PATH isn't set. Please run /set_path with the path to your SC2 install.") + return False + + +class DllDirectory: + # Credit to Black Sliver for this code. + # More info: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + _old: typing.Optional[str] = None + _new: typing.Optional[str] = None + + def __init__(self, new: typing.Optional[str]): + self._new = new + + def __enter__(self): + old = self.get() + if self.set(self._new): + self._old = old + + def __exit__(self, *args): + if self._old is not None: + self.set(self._old) + + @staticmethod + def get() -> str: + if sys.platform == "win32": + n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) + buf = ctypes.create_unicode_buffer(n) + ctypes.windll.kernel32.GetDllDirectoryW(n, buf) + return buf.value + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return None + + @staticmethod + def set(s: typing.Optional[str]) -> bool: + if sys.platform == "win32": + return ctypes.windll.kernel32.SetDllDirectoryW(s) != 0 + # NOTE: other OS may support os.environ["LD_LIBRARY_PATH"], but this fix is windows-specific + return False + + if __name__ == '__main__': colorama.init() asyncio.run(main())