diff --git a/Starcraft2Client.py b/Starcraft2Client.py index f9b6b43f..e9e06335 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())