def __init__(self, reader, writer, token): super().__init__(Platform.Minecraft, __version__, reader, writer, token) self.local_client = LocalClient() self.minecraft_launcher = None self.minecraft_uninstall_command = None self.minecraft_installation_status = LocalGameState.None_ self.minecraft_running_check = None self.tick_count = 0
def __init__(self, reader, writer, token): super().__init__( Platform.RiotGames, # choose platform from available list __version__, # version reader, writer, token, ) self.local_client = LocalClient() self.status = dict.fromkeys(GAME_IDS, LocalGameState.None_) self._update_task = None
def __init__(self, reader, writer, token): super().__init__(Platform.Uplay, __version__, reader, writer, token) self.client = BackendClient(self) self.local_client = LocalClient() self.cached_game_statuses = {} self.games_collection = GamesCollection() self.process_watcher = ProcessWatcher() self.game_status_notifier = GameStatusNotifier(self.process_watcher) self.tick_count = 0 self.updating_games = False self.owned_games_sent = False self.parsing_club_games = False
def __init__(self, reader, writer, token): super().__init__(Platform.Bethesda, __version__, reader, writer, token) self._http_client = AuthenticatedHttpClient(self.store_credentials) self.bethesda_client = BethesdaClient(self._http_client) self.local_client = LocalClient() self.products_cache = product_cache self.owned_games_cache = None self._asked_for_local = False self.update_game_running_status_task = None self.update_game_installation_status_task = None self.check_for_new_games_task = None self.running_games = {} self.launching_lock = None
def __init__(self, reader, writer, token): super().__init__(Platform.RiotGames, __version__, reader, writer, token) self.games_cache = games_cache self._http_client = AuthenticatedHttpClient(self.store_credentials) self._local_client = LocalClient() self.total_games_cache = self.create_total_games_cache() self.friends_cache = [] self.owned_games_cache = [] self.local_games_cache = [] self.running_games_pids = {} self.game_is_loading = True self.checking_for_new_games = False self.updating_game_statuses = False self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer) self.documents_location = self.buffer.value
def __init__(self, reader, writer, token): super().__init__(Platform.Rockstar, __version__, reader, writer, token) self.games_cache = games_cache self._http_client = BackendClient(self.store_credentials) self._local_client = None self.total_games_cache = self.create_total_games_cache() self.friends_cache = [] self.presence_cache = {} self.owned_games_cache = [] self.last_online_game_check = time() - 300 self.local_games_cache = {} self.game_time_cache = {} self.running_games_info_list = {} self.game_is_loading = True self.checking_for_new_games = False self.updating_game_statuses = False self.buffer = None if IS_WINDOWS: self._local_client = LocalClient() self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer) self.documents_location = self.buffer.value
def __init__(self, reader, writer, token): super().__init__(Platform.ParadoxPlaza, __version__, reader, writer, token) self._http_client = AuthenticatedHttpClient(self.store_credentials) self.paradox_client = ParadoxClient(self._http_client) self.local_client = LocalClient() self.owned_games_cache = None self.local_games_cache = {} self.running_game = None self.tick_counter = 0 self.local_games_called = None self.owned_games_called = None self.update_installed_games_task = None self.update_running_games_task = None self.update_owned_games_task = None
class BethesdaPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Bethesda, __version__, reader, writer, token) self._http_client = AuthenticatedHttpClient(self.store_credentials) self.bethesda_client = BethesdaClient(self._http_client) self.local_client = LocalClient() self.local_client.local_games_cache = self.persistent_cache.get('local_games') if not self.local_client.local_games_cache: self.local_client.local_games_cache = {} self.products_cache = product_cache self.owned_games_cache = None self._asked_for_local = False self.update_game_running_status_task = None self.update_game_installation_status_task = None self.betty_client_process_task = None self.check_for_new_games_task = None self.running_games = {} self.launching_lock = None self._tick = 1 async def authenticate(self, stored_credentials=None): if not stored_credentials: return NextStep("web_session", AUTH_PARAMS, cookies=[Cookie("passedICO", "true", ".bethesda.net")], js=JS) try: log.info("Got stored credentials") cookies = pickle.loads(bytes.fromhex(stored_credentials['cookie_jar'])) cookies_parsed = [] for cookie in cookies: if cookie.key in cookies_parsed and cookie.domain: self._http_client.update_cookies({cookie.key: cookie.value}) elif cookie.key not in cookies_parsed: self._http_client.update_cookies({cookie.key: cookie.value}) cookies_parsed.append(cookie.key) log.info("Finished parsing stored credentials, authenticating") user = await self._http_client.authenticate() self._http_client.set_auth_lost_callback(self.lost_authentication) return Authentication(user_id=user['user_id'], user_name=user['display_name']) except (AccessDenied, Banned, UnknownError) as e: log.error(f"Couldn't authenticate with stored credentials {repr(e)}") raise InvalidCredentials() async def pass_login_credentials(self, step, credentials, cookies): cookiez = {} illegal_keys = [''] for cookie in cookies: if cookie['name'] not in illegal_keys: cookiez[cookie['name']] = cookie['value'] self._http_client.update_cookies(cookiez) try: user = await self._http_client.authenticate() except Exception as e: log.error(repr(e)) raise InvalidCredentials() self._http_client.set_auth_lost_callback(self.lost_authentication) return Authentication(user_id=user['user_id'], user_name=user['display_name']) def _check_for_owned_products(self, owned_ids): products_to_consider = [product for product in self.products_cache if 'reference_id' in self.products_cache[product]] owned_product_ids = [] for entitlement_id in owned_ids: for product in products_to_consider: for reference_id in self.products_cache[product]['reference_id']: if entitlement_id in reference_id: self.products_cache[product]['owned'] = True owned_product_ids.append(entitlement_id) return owned_product_ids async def _get_owned_pre_orders(self, pre_order_ids): games_to_send = [] for pre_order in pre_order_ids: pre_order_details = await self.bethesda_client.get_game_details(pre_order) if pre_order_details and 'Entry' in pre_order_details: entries_to_consider = [entry for entry in pre_order_details['Entry'] if 'fields' in entry and 'productName' in entry['fields']] for entry in entries_to_consider: if entry['fields']['productName'] in self.products_cache: self.products_cache[entry['fields']['productName']]['owned'] = True else: games_to_send.append(Game(pre_order, entry['fields']['productName'] + " (Pre Order)", None, LicenseInfo(LicenseType.SinglePurchase))) break return games_to_send def _get_owned_games(self): games_to_send = [] for product in self.products_cache: if self.products_cache[product]["owned"] and self.products_cache[product]["free_to_play"]: games_to_send.append(Game(self.products_cache[product]['local_id'], product, None, LicenseInfo(LicenseType.FreeToPlay))) elif self.products_cache[product]["owned"]: games_to_send.append(Game(self.products_cache[product]['local_id'], product, None, LicenseInfo(LicenseType.SinglePurchase))) return games_to_send async def get_owned_games(self): owned_ids = [] games_to_send = [] try: owned_ids = await self.bethesda_client.get_owned_ids() except (UnknownError, BackendError) as e: log.warning(f"No owned games detected {repr(e)}") log.info(f"Owned Ids: {owned_ids}") product_ids = self._check_for_owned_products(owned_ids) pre_order_ids = set(owned_ids) - set(product_ids) games_to_send.extend(await self._get_owned_pre_orders(pre_order_ids)) games_to_send.extend(self._get_owned_games()) log.info(f"Games to send (with free games): {games_to_send}") self.owned_games_cache = games_to_send return games_to_send async def get_local_games(self): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return [] local_games = [] installed_products = self.local_client.get_installed_products(timeout=2, products_cache=self.products_cache) log.info(f"Installed products {installed_products}") for product in self.products_cache: for installed_product in installed_products: if installed_products[installed_product] == self.products_cache[product]['local_id']: self.products_cache[product]['installed'] = True local_games.append(LocalGame(installed_products[installed_product], LocalGameState.Installed)) self._asked_for_local = True log.info(f"Returning local games {local_games}") return local_games async def prepare_local_size_context(self, game_ids: List[str]) -> Any: return None async def get_local_size(self, game_id: str, context: Any) -> Optional[int]: local_cache = self.local_client.local_games_cache.copy() for game in local_cache: if local_cache[game]['local_id'] == game_id: return await self.local_client.get_size_at_path(local_cache[game]['path']) async def install_game(self, game_id): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return if not self.local_client.is_installed(): await self._open_betty_browser() return if self.local_client.betty_client_process: self.local_client.focus_client_window() await self.launch_game(game_id) else: uuid = None for product in self.products_cache: if self.products_cache[product]['local_id'] == game_id: if self.products_cache[product]['installed']: log.warning("Got install on already installed game, launching") return await self.launch_game(game_id) uuid = "\"" + self.products_cache[product]['uuid'] + "\"" cmd = "\"" + self.local_client.client_exe_path + "\"" + f" --installproduct={uuid}" log.info(f"Calling install game with command {cmd}") subprocess.Popen(cmd, shell=True) async def launch_game(self, game_id): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return if not self.local_client.is_installed(): await self._open_betty_browser() return for product in self.products_cache: if self.products_cache[product]['local_id'] == game_id: if not self.products_cache[product]['installed']: if not self.local_client.betty_client_process: log.warning("Got launch on a not installed game, installing") return await self.install_game(game_id) else: if not self.local_client.betty_client_process: self.launching_lock = time.time() + 45 else: self.launching_lock = time.time() + 30 self.running_games[game_id] = None self.update_local_game_status( LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running)) self.update_game_running_status_task.cancel() cmd = f"start bethesdanet://run/{game_id}" log.info(f"Calling launch command for id {game_id}, {cmd}") subprocess.Popen(cmd, shell=True) async def uninstall_game(self, game_id): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return if not self.local_client.is_installed(): await self._open_betty_browser() return for product in self.products_cache: if self.products_cache[product]['local_id'] == game_id: if not self.products_cache[product]['installed']: return log.info(f"Calling uninstall command for id {game_id}") cmd = f"start bethesdanet://uninstall/{game_id}" subprocess.Popen(cmd, shell=True) if not self.local_client.betty_client_process: await asyncio.sleep(2) # QOL, bethesda slowly reacts to uninstall command, self.local_client.focus_client_window() async def _open_betty_browser(self): url = "https://bethesda.net/game/bethesda-launcher" log.info(f"Opening Bethesda website on url {url}") webbrowser.open(url) async def _heavy_installation_status_check(self): installed_products = self.local_client.get_installed_products(4, self.products_cache) changed = False products_cache_installed_products = {} for product in self.products_cache: if self.products_cache[product]['installed']: products_cache_installed_products[product] = self.products_cache[product]['local_id'] for installed_product in installed_products: if installed_product not in products_cache_installed_products: self.products_cache[installed_product]["installed"] = True self.update_local_game_status(LocalGame(installed_products[installed_product], LocalGameState.Installed)) changed = True for installed_product in products_cache_installed_products: if installed_product not in installed_products: self.products_cache[installed_product]["installed"] = False self.update_local_game_status(LocalGame(products_cache_installed_products[installed_product], LocalGameState.None_)) changed = True return changed def _light_installation_status_check(self): changed = False for local_game in self.local_client.local_games_cache: local_game_installed = self.local_client.is_local_game_installed(self.local_client.local_games_cache[local_game]) if local_game_installed and not self.products_cache[local_game]["installed"]: self.products_cache[local_game]["installed"] = True self.update_local_game_status(LocalGame(self.local_client.local_games_cache[local_game]['local_id'], LocalGameState.Installed)) changed = True elif not local_game_installed and self.products_cache[local_game]["installed"]: self.products_cache[local_game]["installed"] = False self.update_local_game_status(LocalGame(self.local_client.local_games_cache[local_game]['local_id'], LocalGameState.None_)) changed = True return changed async def update_game_installation_status(self): if self.local_client.clientgame_changed() or self.local_client.launcher_children_number_changed(): await asyncio.sleep(1) log.info("Starting heavy installation status check") if await self._heavy_installation_status_check(): # Game status has changed self.persistent_cache['local_games'] = self.local_client.local_games_cache self.push_cache() else: if self._light_installation_status_check(): # Game status has changed self.persistent_cache['local_games'] = self.local_client.local_games_cache self.push_cache() async def _scan_running_games(self, process_iter_interval): for process in process_iter(): await asyncio.sleep(process_iter_interval) for local_game in self.local_client.local_games_cache: if not process.binary_path: continue if process.binary_path in self.local_client.local_games_cache[local_game]['execs']: log.info(f"Found a running game! {local_game}") local_id = self.local_client.local_games_cache[local_game]['local_id'] if local_id not in self.running_games: self.update_local_game_status(LocalGame(local_id, LocalGameState.Installed | LocalGameState.Running)) self.running_games[local_id] = RunningGame(self.local_client.local_games_cache[local_game]['execs'], process) async def _update_status_of_already_running_games(self, process_iter_interval, dont_downgrade_status): for running_game in self.running_games.copy(): if not self.running_games[running_game] and dont_downgrade_status: log.info(f"Found 'just launched' game {running_game}") continue elif not self.running_games[running_game]: log.info(f"Found 'just launched' game but its still without pid and its time run out {running_game}") self.running_games.pop(running_game) self.update_local_game_status( LocalGame(running_game, LocalGameState.Installed)) continue for process in process_iter(): await asyncio.sleep(process_iter_interval) if process.binary_path in self.running_games[running_game].execs: return True self.running_games.pop(running_game) self.update_local_game_status( LocalGame(running_game, LocalGameState.Installed)) async def update_game_running_status(self): process_iter_interval = 0.02 dont_downgrade_status = False if self.launching_lock and self.launching_lock >= time.time(): process_iter_interval = 0.01 dont_downgrade_status = True if self.running_games: # Don't iterate over processes if a game is already running, assuming user is playing one game at a time. if not await self._update_status_of_already_running_games(process_iter_interval, dont_downgrade_status): await self._scan_running_games(process_iter_interval) await asyncio.sleep(1) return await self._scan_running_games(process_iter_interval) await asyncio.sleep(1) async def check_for_new_games(self): games_cache = self.owned_games_cache owned_games = await self.get_owned_games() for owned_game in owned_games: if owned_game not in games_cache: self.add_game(owned_game) await asyncio.sleep(60) async def close_bethesda_window(self): if sys.platform != 'win32': return window_name = "Bethesda.net Launcher" max_delay = 10 intermediate_sleep = 0.05 stop_time = time.time() + max_delay def timed_out(): if time.time() >= stop_time: log.warning(f"Timed out trying to close {window_name}") return True return False try: hwnd = ctypes.windll.user32.FindWindowW(None, window_name) while not ctypes.windll.user32.IsWindowVisible(hwnd): hwnd = ctypes.windll.user32.FindWindowW(None, window_name) await asyncio.sleep(intermediate_sleep) if timed_out(): return while ctypes.windll.user32.IsWindowVisible(hwnd): await asyncio.sleep(intermediate_sleep) ctypes.windll.user32.CloseWindow(hwnd) if timed_out(): return except Exception as e: log.error(f"Exception when checking if window is visible {repr(e)}") async def shutdown_platform_client(self): if sys.platform != 'win32': return log.info("killing bethesda") subprocess.Popen("taskkill.exe /im \"BethesdaNetLauncher.exe\"") async def launch_platform_client(self): if not self.local_client.betty_client_process: return if sys.platform != 'win32': return log.info("launching bethesda") subprocess.Popen('start bethesdanet://', shell=True) asyncio.create_task(self.close_bethesda_window()) def tick(self): if sys.platform == 'win32': if self._asked_for_local and (not self.update_game_installation_status_task or self.update_game_installation_status_task.done()): self.update_game_installation_status_task = asyncio.create_task(self.update_game_installation_status()) if self._asked_for_local and (not self.update_game_running_status_task or self.update_game_running_status_task.done()): self.update_game_running_status_task = asyncio.create_task(self.update_game_running_status()) if not self.betty_client_process_task or self.betty_client_process_task.done(): self.betty_client_process_task = asyncio.create_task(self.local_client.is_running()) if self._http_client.bearer: if self.owned_games_cache and (not self.check_for_new_games_task or self.check_for_new_games_task.done()): self.check_for_new_games_task = asyncio.create_task(self.check_for_new_games()) async def shutdown(self): await self._http_client.close()
class RiotPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__( Platform.RiotGames, # choose platform from available list __version__, # version reader, writer, token, ) self.local_client = LocalClient() self.status = dict.fromkeys(GAME_IDS, LocalGameState.None_) self._update_task = None async def authenticate(self, stored_credentials=None): self.store_credentials({"dummy": "dummy"}) return Authentication("riot_user", "Riot User") async def get_owned_games(self): log.info("Getting owned games") return [ Game( GameID.league_of_legends, "League of Legends", None, LicenseInfo(LicenseType.FreeToPlay), ), Game( GameID.legends_of_runeterra, "Legends of Runeterra", None, LicenseInfo(LicenseType.FreeToPlay), ), Game( GameID.valorant, "Valorant", None, LicenseInfo(LicenseType.FreeToPlay), ), ] async def get_local_games(self): log.info("Getting local games") local_games = [] for game_id in GAME_IDS: if self.local_client.game_installed(game_id): local_game = LocalGame(game_id, LocalGameState.Installed) local_games.append(local_game) log.debug(f"RIOT_INSTALLED_GAMES: {local_games}") return local_games async def prepare_local_size_context(self, game_ids): sizes = [] for game_id in GAME_IDS: size = await misc.get_size_at_path( self.local_client.install_location[game_id], if_none=0 ) if game_id == GameID.valorant: size += await misc.get_size_at_path( self.local_client.install_location[GameID.vanguard], if_none=0 ) if size == 0: size = None sizes.append(size) return dict(zip(game_ids, sizes)) async def get_local_size(self, game_id: str, context): return context[game_id] async def uninstall_game(self, game_id): self.local_client.update_installed() self.local_client.uninstall(game_id) async def launch_game(self, game_id): log.debug("RCS location: " + self.local_client.riot_client_services_path) self.local_client.update_installed() self.local_client.launch(game_id) async def get_os_compatibility(self, game_id, context): log.info("Getting os compatibility") if game_id == GameID.league_of_legends: return OSCompatibility.Windows | OSCompatibility.MacOS elif game_id == GameID.legends_of_runeterra: return OSCompatibility.Windows elif game_id == GameID.valorant: return OSCompatibility.Windows async def install_game(self, game_id): log.info("Installing game") self.local_client.update_installed() if self.local_client.riot_client_services_path is None: misc.download(DOWNLOAD_URL[game_id], misc.open_path) else: self.local_client.launch(game_id, save_process=False) async def _update(self): def update(game_id, status: LocalGameState): if self.status[game_id] != status: self.status[game_id] = status self.update_local_game_status(LocalGame(game_id, status)) log.info(f"Updated {game_id} to {status}") return True # return true if needed to update return False self.local_client.update_installed() for game_id in GAME_IDS: if self.local_client.game_running(game_id): if update(game_id, LocalGameState.Installed | LocalGameState.Running): self.game_time_tracker.start_tracking_game(game_id) elif self.local_client.game_installed(game_id): if update(game_id, LocalGameState.Installed): if game_id in self.game_time_tracker.get_tracking_games(): self.game_time_tracker.stop_tracking_game(game_id) else: update(game_id, LocalGameState.None_) log.debug(f"self.local_client.install_location: {self.local_client.install_location}") log.debug(f"self.status: {self.status}") await asyncio.sleep(5) def tick(self): if self._update_task is None or self._update_task.done(): self._update_task = self.create_task(self._update(), "Update Task") # Time Tracker async def get_game_time(self, game_id, context): try: return self.game_time_tracker.get_tracked_time(game_id) except time_tracker.GameNotTrackedException: return None def handshake_complete(self): misc.cleanup() self.game_time_cache = None if "game_time_cache" in self.persistent_cache: self.game_time_cache = pickle.loads( bytes.fromhex(self.persistent_cache["game_time_cache"]) ) else: if os.path.isfile(LOCAL_FILE_CACHE): with open(LOCAL_FILE_CACHE, "r") as file: for line in file.readlines(): if line[:1] != "#": self.game_time_cache = pickle.loads(bytes.fromhex(line)) break self.game_time_tracker = time_tracker.TimeTracker(game_time_cache=self.game_time_cache) async def shutdown(self): misc.kill_all_processes() for game_id in self.game_time_tracker.get_tracking_games(): self.game_time_tracker.stop_tracking_game(game_id) if self.game_time_cache is not None: with open(LOCAL_FILE_CACHE, "w+") as file: file.write("# DO NOT EDIT THIS FILE\n") file.write(self.game_time_tracker.get_time_cache_hex()) log.info("Wrote to local file cache") await super().shutdown() def game_times_import_complete(self): if len(self.game_time_tracker.get_tracking_games()) > 0: log.debug("Game time still being tracked. Not setting cache yet.") else: self.game_time_cache = self.game_time_tracker.get_time_cache() log.debug(f"game_time_cache: {self.game_time_cache}") self.persistent_cache["game_time_cache"] = self.game_time_tracker.get_time_cache_hex() self.push_cache()
class RockstarPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.RiotGames, __version__, reader, writer, token) self.games_cache = games_cache self._http_client = AuthenticatedHttpClient(self.store_credentials) self._local_client = LocalClient() self.total_games_cache = self.create_total_games_cache() self.friends_cache = [] self.owned_games_cache = [] self.local_games_cache = {} self.running_games_pids = {} self.game_is_loading = True self.checking_for_new_games = False self.updating_game_statuses = False self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer) self.documents_location = self.buffer.value def is_authenticated(self): return self._http_client.is_authenticated() async def authenticate(self, stored_credentials=None): self._http_client.create_session(stored_credentials) if not stored_credentials: return NextStep("web_session", AUTH_PARAMS) try: log.info("INFO: The credentials were successfully obtained.") cookies = pickle.loads( bytes.fromhex(stored_credentials['session_object'])).cookies log.debug("ROCKSTAR_COOKIES_FROM_HEX: " + str(cookies)) for cookie in cookies: cookie_object = { "name": cookie.name, "value": cookie.value, "domain": cookie.domain, "path": cookie.path } self._http_client.update_cookie(cookie_object) self._http_client.set_current_auth_token( stored_credentials['current_auth_token']) log.info( "INFO: The stored credentials were successfully parsed. Beginning authentication..." ) user = await self._http_client.authenticate() return Authentication(user_id=user['rockstar_id'], user_name=user['display_name']) except Exception as e: log.warning( "ROCKSTAR_AUTH_WARNING: The exception " + repr(e) + " was thrown, presumably because of " "outdated credentials. Attempting to get new credentials...") self._http_client.set_auth_lost_callback(self.lost_authentication) try: user = await self._http_client.authenticate() return Authentication(user_id=user['rockstar_id'], user_name=user['display_name']) except Exception as e: log.error( "ROCKSTAR_AUTH_FAILURE: Something went terribly wrong with the re-authentication. " + repr(e)) log.exception("ROCKSTAR_STACK_TRACE") raise InvalidCredentials() async def pass_login_credentials(self, step, credentials, cookies): log.debug("ROCKSTAR_COOKIE_LIST: " + str(cookies)) for cookie in cookies: if cookie['name'] == "ScAuthTokenData": self._http_client.set_current_auth_token(cookie['value']) cookie_object = { "name": cookie['name'], "value": cookie['value'], "domain": cookie['domain'], "path": cookie['path'] } self._http_client.update_cookie(cookie_object) try: user = await self._http_client.authenticate() except Exception as e: log.error(repr(e)) raise InvalidCredentials() return Authentication(user_id=user["rockstar_id"], user_name=user["display_name"]) async def shutdown(self): await self._http_client.close() def create_total_games_cache(self): cache = [] for title_id in list(games_cache): cache.append(self.create_game_from_title_id(title_id)) return cache async def get_friends(self): # This method is currently causing the plugin to crash after its second call. I am unsure of why this is # happening, but it needs to be fixed. # NOTE: This will return a list of type FriendInfo. # The Social Club website returns a list of the current user's friends through the url # https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&pageIndex=0&pageSize=30. # The nickname URL parameter is left blank because the website instead uses the bearer token to get the correct # information. The last two parameters are of great importance, however. The parameter pageSize determines the # number of friends given on that page's list, while pageIndex keeps track of the page that the information is # on. The maximum number for pageSize is 30, so that is what we will use to cut down the number of HTTP # requests. # We first need to get the number of friends. url = ( "https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&" "pageIndex=0&pageSize=30") current_page = await self._http_client.get_json_from_request_strict(url ) log.debug("ROCKSTAR_FRIENDS_REQUEST: " + str(current_page)) num_friends = current_page['rockstarAccountList']['totalFriends'] num_pages_required = num_friends / 30 if num_friends % 30 != 0 else ( num_friends / 30) - 1 # Now, we need to get the information about the friends. friends_list = current_page['rockstarAccountList']['rockstarAccounts'] return_list = [] for i in range(0, len(friends_list)): friend = FriendInfo(friends_list[i]['rockstarId'], friends_list[i]['displayName']) return_list.append(FriendInfo) for cached_friend in self.friends_cache: if cached_friend.user_id == friend.user_id: break else: self.friends_cache.append(friend) self.add_friend(friend) log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name + " (Rockstar ID: " + str(friend.user_id) + ")") # The first page is finished, but now we need to work on any remaining pages. if num_pages_required > 0: for i in range(1, int(num_pages_required + 1)): url = ( "https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&" "pageIndex=" + str(i) + "&pageSize=30") return_list.append(friend for friend in await self._get_friends(url)) return return_list async def _get_friends(self, url): current_page = self._http_client.get_json_from_request_strict(url) friends_list = current_page['rockstarAccountList']['rockstarAccounts'] return_list = [] for i in range(0, len(friends_list)): friend = FriendInfo(friends_list[i]['rockstarId'], friends_list[i]['displayName']) return_list.append(FriendInfo) for cached_friend in self.friends_cache: if cached_friend.user_id == friend.user_id: break else: # An else-statement occurs after a for-statement if the latter finishes WITHOUT breaking. self.friends_cache.append(friend) self.add_friend(friend) log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name + " (Rockstar ID: " + str(friend.user_id) + ")") return return_list async def get_owned_games(self): # Here is the actual implementation of getting the user's owned games: # -Get the list of games_played from rockstargames.com/auth/get-user.json. # -If possible, use the launcher log to confirm which games are actual launcher games and which are # Steam/Retail games. # -If it is not possible to use the launcher log, then just use the list provided by the website. if not self.is_authenticated(): raise AuthenticationRequired() # Get the list of games_played from https://www.rockstargames.com/auth/get-user.json. owned_title_ids = [] online_check_success = True try: played_games = await self._http_client.get_played_games() for game in played_games: title_id = get_game_title_id_from_online_title_id(game) owned_title_ids.append(title_id) log.debug("ROCKSTAR_ONLINE_GAME: Found played game " + title_id + "!") except Exception as e: log.error( "ROCKSTAR_PLAYED_GAMES_ERROR: The exception " + repr(e) + " was thrown when attempting to get the" " user's played games online. Falling back to log file check..." ) online_check_success = False # The log is in the Documents folder. current_log_count = 0 log_file = None log_file_append = "" # The Rockstar Games Launcher generates 10 log files before deleting them in a FIFO fashion. Old log files are # given a number ranging from 1 to 9 in their name. In case the first log file does not have all of the games, # we need to check the other log files, if possible. while current_log_count < 10: try: if current_log_count != 0: log_file_append = ".0" + str(current_log_count) log_file = os.path.join( self.documents_location, "Rockstar Games\\Launcher\\launcher" + log_file_append + ".log") log.debug("ROCKSTAR_LOG_LOCATION: Checking the file " + log_file + "...") owned_title_ids = await self.parse_log_file( log_file, owned_title_ids, online_check_success) break except NoGamesInLogException: log.warning( "ROCKSTAR_LOG_WARNING: There are no owned games listed in " + str(log_file) + ". Moving to " "the next log file...") current_log_count += 1 except NoLogFoundException: log.warning( "ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " "from. Assuming that the online list is correct...") break except Exception: # This occurs after ROCKSTAR_LOG_ERROR. break if current_log_count == 10: log.warning( "ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " "from. Assuming that the online list is correct...") remove_all = False if len(self.owned_games_cache) == 0: remove_all = True for title_id in owned_title_ids: game = self.create_game_from_title_id(title_id) if game not in self.owned_games_cache: log.debug("ROCKSTAR_ADD_GAME: Adding " + title_id + " to owned games cache...") self.add_game(game) self.owned_games_cache.append(game) if remove_all is True: for key, value in games_cache.items(): if key not in owned_title_ids: log.debug("ROCKSTAR_REMOVE_GAME: Removing " + key + " from owned games cache...") self.remove_game(value['rosTitleId']) else: for game in self.owned_games_cache: if get_game_title_id_from_ros_title_id( game.game_id) not in owned_title_ids: log.debug( "ROCKSTAR_REMOVE_GAME: Removing " + get_game_title_id_from_ros_title_id(game.game_id) + " from owned games cache...") self.remove_game(game.game_id) self.owned_games_cache.remove(game) return self.owned_games_cache @staticmethod async def parse_log_file(log_file, owned_title_ids, online_check_success): owned_title_ids_ = owned_title_ids checked_games_count = 0 total_games_count = len(games_cache) if os.path.exists(log_file): with FileReadBackwards(log_file, encoding="utf-8") as frb: while checked_games_count < total_games_count: try: line = frb.readline() except UnicodeDecodeError: log.warning( "ROCKSTAR_LOG_UNICODE_WARNING: An invalid Unicode character was found in the line " + line + ". Continuing to next line...") continue except Exception as e: log.error( "ROCKSTAR_LOG_ERROR: Reading " + line + " from the log file resulted in the " "exception " + repr(e) + " being thrown. Using the online list... (Please report " "this issue on the plugin's GitHub page!)") raise if not line: log.error( "ROCKSTAR_LOG_FINISHED_ERROR: The entire log file was read, but all of the games " "could not be accounted for. Proceeding to import the games that have been " "confirmed...") raise NoGamesInLogException() # We need to do two main things with the log file: # 1. If a game is present in owned_title_ids but not owned according to the log file, then it is # assumed to be a non-Launcher game, and is removed from the list. # 2. If a game is owned according to the log file but is not already present in owned_title_ids, # then it is assumed that the user has purchased the game on the Launcher, but has not yet played # it. In this case, the game will be added to owned_title_ids. if ("launcher" not in line) and ("on branch " in line): # Found a game! # Each log line for a title branch report describes the title id of the game starting at # character 65. Interestingly, the lines all have the same colon as character 75. This implies # that this format was intentionally done by Rockstar, so they likely will not change it anytime # soon. title_id = line[65:75].strip() log.debug( "ROCKSTAR_LOG_GAME: The game with title ID " + title_id + " is owned!") if title_id not in owned_title_ids_: if online_check_success is True: # Case 2: The game is owned, but has not been played. log.warning( "ROCKSTAR_UNPLAYED_GAME: The game with title ID " + title_id + " is owned, but it has never been played!") owned_title_ids_.append(title_id) checked_games_count += 1 elif "no branches!" in line: title_id = line[65:75].strip() if title_id in owned_title_ids_: # Case 1: The game is not actually owned on the launcher. log.warning( "ROCKSTAR_FAKE_GAME: The game with title ID " + title_id + " is not owned on " "the Rockstar Games Launcher!") owned_title_ids_.remove(title_id) checked_games_count += 1 if checked_games_count == total_games_count: break return owned_title_ids_ else: raise NoLogFoundException() async def get_local_games(self): # Since the API requires that get_local_games returns a list of LocalGame objects, local_list is the value that # needs to be returned. However, for internal use (the self.local_games_cache field), the dictionary local_games # is used for greater flexibility. local_games = {} local_list = [] for game in self.total_games_cache: title_id = get_game_title_id_from_ros_title_id(str(game.game_id)) check = self._local_client.get_path_to_game(title_id) if check is not None: if (title_id in self.running_games_pids and check_if_process_exists( self.running_games_pids[title_id][0])): local_game = self.create_local_game_from_title_id( title_id, True, True) else: local_game = self.create_local_game_from_title_id( title_id, False, True) else: local_game = self.create_local_game_from_title_id( title_id, False, False) local_games[title_id] = local_game local_list.append(local_game) self.local_games_cache = local_games log.debug("ROCKSTAR_INSTALLED_GAMES: " + str(local_games)) return local_list async def check_for_new_games(self): self.checking_for_new_games = True await self.get_owned_games() await asyncio.sleep(60) self.checking_for_new_games = False async def check_game_statuses(self): self.updating_game_statuses = True old_local_game_cache = self.local_games_cache await self.get_local_games() for title_id, local_game in self.local_games_cache.items(): if (local_game.local_game_state != (old_local_game_cache[title_id]).local_game_state): log.debug("ROCKSTAR_LOCAL_CHANGE: The status for " + title_id + " has changed.") self.update_local_game_status(local_game) # else: # log.debug("ROCKSTAR_LOCAL_REMAIN: The status for " + title_id + " has not changed.") - Reduce Console # Spam (Enable this if you need to.) await asyncio.sleep(5) self.updating_game_statuses = False async def launch_game(self, game_id): title_id = get_game_title_id_from_ros_title_id(game_id) self.running_games_pids[title_id] = [ await self._local_client.launch_game_from_title_id(title_id), True ] log.debug("ROCKSTAR_PIDS: " + str(self.running_games_pids)) if self.running_games_pids[title_id][0] != '-1': self.update_local_game_status( LocalGame(game_id, LocalGameState.Running | LocalGameState.Installed)) async def install_game(self, game_id): title_id = get_game_title_id_from_ros_title_id(game_id) log.debug("ROCKSTAR_INSTALL_REQUEST: Requesting to install " + title_id + "...") self._local_client.install_game_from_title_id(title_id) # If the game is not released yet, then we should allow them to see this on the Rockstar Games Launcher, but the # game's installation status should not be changed. if games_cache[title_id]["isPreOrder"]: return self.update_local_game_status( LocalGame(game_id, LocalGameState.Installed)) async def uninstall_game(self, game_id): title_id = get_game_title_id_from_ros_title_id(game_id) log.debug("ROCKSTAR_UNINSTALL_REQUEST: Requesting to uninstall " + title_id + "...") self._local_client.uninstall_game_from_title_id(title_id) self.update_local_game_status(LocalGame(game_id, LocalGameState.None_)) def create_game_from_title_id(self, title_id): return Game(self.games_cache[title_id]["rosTitleId"], self.games_cache[title_id]["friendlyName"], None, self.games_cache[title_id]["licenseInfo"]) def create_local_game_from_title_id(self, title_id, is_running, is_installed): if is_running: return LocalGame(self.games_cache[title_id]["rosTitleId"], LocalGameState.Running | LocalGameState.Installed) elif is_installed: return LocalGame(self.games_cache[title_id]["rosTitleId"], LocalGameState.Installed) else: return LocalGame(self.games_cache[title_id]["rosTitleId"], LocalGameState.None_) def tick(self): if not self.is_authenticated(): return if not self.checking_for_new_games: log.debug("Checking for new games...") asyncio.create_task(self.check_for_new_games()) if not self.updating_game_statuses: log.debug("Checking local game statuses...") asyncio.create_task(self.check_game_statuses())
class RockstarPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Rockstar, __version__, reader, writer, token) self.games_cache = games_cache self._http_client = BackendClient(self.store_credentials) self._local_client = None self.total_games_cache = self.create_total_games_cache() self.friends_cache = [] self.presence_cache = {} self.owned_games_cache = [] self.last_online_game_check = time() - 300 self.local_games_cache = {} self.game_time_cache = {} self.running_games_info_list = {} self.game_is_loading = True self.checking_for_new_games = False self.updating_game_statuses = False self.buffer = None if IS_WINDOWS: self._local_client = LocalClient() self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer) self.documents_location = self.buffer.value def is_authenticated(self): return self._http_client.is_authenticated() @staticmethod def loads_js(file): with open(os.path.abspath(os.path.join(__file__, '..', 'js', file)), 'r') as f: return f.read() def handshake_complete(self): game_time_cache_in_persistent_cache = False for key, value in self.persistent_cache.items(): # if "achievements_" in key: # log.debug("ROCKSTAR_CACHE_IMPORT: Importing " + key + " from persistent cache...") # self._all_achievements_cache[key] = pickle.loads(bytes.fromhex(value)) if key == "game_time_cache": self.game_time_cache = pickle.loads(bytes.fromhex(value)) game_time_cache_in_persistent_cache = True if IS_WINDOWS and not game_time_cache_in_persistent_cache: # The game time cache was not found in the persistent cache, so the plugin will instead attempt to get the # cache from the user's file stored on their disk. file_location = os.path.join(self.documents_location, "RockstarPlayTimeCache.txt") try: file = open(file_location, "r") for line in file.readlines(): if line[:1] != "#": log.debug("ROCKSTAR_LOCAL_GAME_TIME_FROM_FILE: " + str(pickle.loads(bytes.fromhex(line)))) self.game_time_cache = pickle.loads(bytes.fromhex(line)) break if not self.game_time_cache: log.warning("ROCKSTAR_NO_GAME_TIME: The user's played time could not be found in neither the " "persistent cache nor the designated local file. Let's hope that the user is new...") except FileNotFoundError: log.warning("ROCKSTAR_NO_GAME_TIME: The user's played time could not be found in neither the persistent" " cache nor the designated local file. Let's hope that the user is new...") async def authenticate(self, stored_credentials=None): try: self._http_client.create_session(stored_credentials) except KeyError: log.error("ROCKSTAR_OLD_LOG_IN: The user has likely previously logged into the plugin with a version less " "than v0.3, and their credentials might be corrupted. Forcing a log-out...") raise InvalidCredentials() if not stored_credentials: # We will create the fingerprint JavaScript dictionary here. fingerprint_js = { r'https://www.rockstargames.com/': [ self.loads_js("fingerprint2.js"), self.loads_js("HashGen.js"), self.loads_js("GenerateFingerprint.js") ] } return NextStep("web_session", AUTH_PARAMS, js=fingerprint_js) try: log.info("INFO: The credentials were successfully obtained.") if LOG_SENSITIVE_DATA: cookies = pickle.loads(bytes.fromhex(stored_credentials['cookie_jar'])) log.debug("ROCKSTAR_COOKIES_FROM_HEX: " + str(cookies)) # sensitive data hidden by default # for cookie in cookies: # self._http_client.update_cookies({cookie.name: cookie.value}) self._http_client.set_current_auth_token(stored_credentials['current_auth_token']) self._http_client.set_current_sc_token(stored_credentials['current_sc_token']) self._http_client.set_refresh_token_absolute( pickle.loads(bytes.fromhex(stored_credentials['refresh_token']))) self._http_client.set_fingerprint(stored_credentials['fingerprint']) log.info("INFO: The stored credentials were successfully parsed. Beginning authentication...") user = await self._http_client.authenticate() return Authentication(user_id=user['rockstar_id'], user_name=user['display_name']) except (NetworkError, UnknownError): raise except Exception as e: log.warning("ROCKSTAR_AUTH_WARNING: The exception " + repr(e) + " was thrown, presumably because of " "outdated credentials. Attempting to get new credentials...") self._http_client.set_auth_lost_callback(True) try: user = await self._http_client.authenticate() return Authentication(user_id=user['rockstar_id'], user_name=user['display_name']) except Exception as e: log.error("ROCKSTAR_AUTH_FAILURE: Something went terribly wrong with the re-authentication. " + repr(e)) log.exception("ROCKSTAR_STACK_TRACE") raise InvalidCredentials async def pass_login_credentials(self, step, credentials, cookies): if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_COOKIE_LIST: " + str(cookies)) for cookie in cookies: if cookie['name'] == "ScAuthTokenData": self._http_client.set_current_auth_token(cookie['value']) if cookie['name'] == "BearerToken": self._http_client.set_current_sc_token(cookie['value']) if cookie['name'] == "RMT": if cookie['value'] != "": if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: " + cookie['value']) else: log.debug("ROCKSRAR_REMEMBER_ME: Got RMT: ***") # Only asterisks are shown here for consistency # with the output when the user has a blank RMT from multi-factor authentication. self._http_client.set_refresh_token(cookie['value']) else: if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: [Blank!]") else: log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: ***") self._http_client.set_refresh_token('') if cookie['name'] == "fingerprint": if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_FINGERPRINT: Got fingerprint: " + cookie['value'].replace("$", ";")) else: log.debug("ROCKSTAR_FINGERPRINT: Got fingerprint: ***") self._http_client.set_fingerprint(cookie['value'].replace("$", ";")) # We will not add the fingerprint as a cookie to the session; it will instead be stored with the user's # credentials. continue if re.search("^rsso", cookie['name']): if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_RSSO: Got " + cookie['name'] + ": " + cookie['value']) else: log.debug(f"ROCKSTAR_RSSO: Got rsso-***: {cookie['value'][:5]}***{cookie['value'][-3:]}") cookie_object = { "name": cookie['name'], "value": cookie['value'], "domain": cookie['domain'], "path": cookie['path'] } self._http_client.update_cookie(cookie_object) try: user = await self._http_client.authenticate() except Exception as e: log.error(repr(e)) raise InvalidCredentials return Authentication(user_id=user["rockstar_id"], user_name=user["display_name"]) async def shutdown(self): # At this point, we can write to a file to keep a cached copy of the user's played time. # This will prevent the play time from being erased if the user loses authentication. if IS_WINDOWS and self.game_time_cache: # For the sake of convenience, we will store this file in the user's Documents folder. # Obviously, this feature is only compatible with (and relevant for) Windows machines. file_location = os.path.join(self.documents_location, "RockstarPlayTimeCache.txt") file = open(file_location, "w+") file.write("# This file contains a cached copy of the user's play time for the Rockstar plugin for GOG " "Galaxy 2.0.\n") file.write("# DO NOT EDIT THIS FILE IN ANY WAY, LEST THE CACHE GETS CORRUPTED AND YOUR PLAY TIME IS LOST!\n" ) file.write(pickle.dumps(self.game_time_cache).hex()) file.close() await self._http_client.close() await super().shutdown() def create_total_games_cache(self): cache = [] for title_id in list(games_cache): cache.append(self.create_game_from_title_id(title_id)) return cache if ARE_ACHIEVEMENTS_IMPLEMENTED: async def get_unlocked_achievements(self, game_id, context): # The Social Club API has an authentication endpoint located at https://scapi.rockstargames.com/ # achievements/awardedAchievements?title=[game-id]&platform=pc&rockstarId=[rockstar-ID], which returns a # list of the user's unlocked achievements for the specified game. It uses the Social Club standard for # authentication (a request header named Authorization containing "Bearer [Bearer-Token]"). title_id = get_game_title_id_from_ros_title_id(game_id) if games_cache[title_id]["achievementId"] is None or \ (games_cache[title_id]["isPreOrder"]): return [] log.debug("ROCKSTAR_ACHIEVEMENT_CHECK: Beginning achievements check for " + title_id + " (Achievement ID: " + get_achievement_id_from_ros_title_id(game_id) + ")...") # Now, we can begin getting the user's achievements for the specified game. achievement_id = get_achievement_id_from_ros_title_id(game_id) url = (f"https://scapi.rockstargames.com/achievements/awardedAchievements?title={achievement_id}" f"&platform=pc&rockstarId={self._http_client.get_rockstar_id()}") unlocked_achievements = await self._http_client.get_json_from_request_strict(url) achievements_dict = unlocked_achievements["awardedAchievements"] achievements_list = [] for key, value in achievements_dict.items(): # What if an achievement is added to the Social Club after the cache was already made? In this event, we # need to refresh the cache. achievement_num = key unlock_time = await get_unix_epoch_time_from_date(value["dateAchieved"]) achievements_list.append(Achievement(unlock_time, achievement_id=achievement_num)) return achievements_list async def get_friends(self) -> List[UserInfo]: # The Social Club website returns a list of the current user's friends through the url # https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&pageIndex=0&pageSize=30. # The nickname URL parameter is left blank because the website instead uses the bearer token to get the correct # information. The last two parameters are of great importance, however. The parameter pageSize determines the # number of friends given on that page's list, while pageIndex keeps track of the page that the information is # on. The maximum number for pageSize is 30, so that is what we will use to cut down the number of HTTP # requests. # We first need to get the number of friends. url = ("https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&" "pageIndex=0&pageSize=30") try: current_page = await self._http_client.get_json_from_request_strict(url) except TimeoutError: log.warning("ROCKSTAR_FRIENDS_TIMEOUT: The request to get the user's friends at page index 0 timed out. " "Returning the cached list...") return self.friends_cache if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_FRIENDS_REQUEST: " + str(current_page)) else: log.debug("ROCKSTAR_FRIENDS_REQUEST: ***") num_friends = current_page['rockstarAccountList']['totalFriends'] num_pages_required = num_friends / 30 if num_friends % 30 != 0 else (num_friends / 30) - 1 # Now, we need to get the information about the friends. friends_list = current_page['rockstarAccountList']['rockstarAccounts'] return_list = await self._parse_friends(friends_list) # The first page is finished, but now we need to work on any remaining pages. if num_pages_required > 0: for i in range(1, int(num_pages_required + 1)): try: url = ("https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&" "pageIndex=" + str(i) + "&pageSize=30") for friend in await self._get_friends(url): return_list.append(friend) except TimeoutError: log.warning(f"ROCKSTAR_FRIENDS_TIMEOUT: The request to get the user's friends at page index {i} " f"timed out. Returning the cached list...") return self.friends_cache return return_list async def _get_friends(self, url: str) -> List[UserInfo]: try: current_page = await self._http_client.get_json_from_request_strict(url) except TimeoutError: raise friends_list = current_page['rockstarAccountList']['rockstarAccounts'] return await self._parse_friends(friends_list) async def _parse_friends(self, friends_list: dict) -> List[UserInfo]: return_list = [] for i in range(0, len(friends_list)): avatar_uri = f"https://a.rsg.sc/n/{friends_list[i]['displayName'].lower()}/l" profile_uri = f"https://socialclub.rockstargames.com/member/{friends_list[i]['displayName']}/" friend = UserInfo(user_id=str(friends_list[i]['rockstarId']), user_name=friends_list[i]['displayName'], avatar_url=avatar_uri, profile_url=profile_uri) return_list.append(friend) for cached_friend in self.friends_cache: if cached_friend.user_id == friend.user_id: break else: # An else-statement occurs after a for-statement if the latter finishes WITHOUT breaking. self.friends_cache.append(friend) if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name + " (Rockstar ID: " + str(friend.user_id) + ")") else: log.debug(f"ROCKSTAR_FRIEND: Found {friend.user_name[:1]}*** (Rockstar ID: ***)") return return_list async def get_owned_games_online(self): # Get the list of games_played from https://socialclub.rockstargames.com/ajax/getGoogleTagManagerSetupData. owned_title_ids = [] online_check_success = True self.last_online_game_check = time() try: played_games = await self._http_client.get_played_games() for game in played_games: owned_title_ids.append(game) log.debug("ROCKSTAR_ONLINE_GAME: Found played game " + game + "!") except Exception as e: log.error("ROCKSTAR_PLAYED_GAMES_ERROR: The exception " + repr(e) + " was thrown when attempting to get" " the user's played games online. Falling back to log file check...") online_check_success = False return owned_title_ids, online_check_success async def get_owned_games(self, owned_title_ids=None, online_check_success=False): # Here is the actual implementation of getting the user's owned games: # -Get the list of games_played from rockstargames.com/auth/get-user.json. # -If possible, use the launcher log to confirm which games are actual launcher games and which are # Steam/Retail games. # -If it is not possible to use the launcher log, then just use the list provided by the website. if owned_title_ids is None: owned_title_ids = [] if not self.is_authenticated(): raise AuthenticationRequired() # The log is in the Documents folder. current_log_count = 0 log_file = None log_file_append = "" # The Rockstar Games Launcher generates 10 log files before deleting them in a FIFO fashion. Old log files are # given a number ranging from 1 to 9 in their name. In case the first log file does not have all of the games, # we need to check the other log files, if possible. while current_log_count < 10: # We need to prevent the log file check for Mac users. if not IS_WINDOWS: break try: if current_log_count != 0: log_file_append = ".0" + str(current_log_count) log_file = os.path.join(self.documents_location, "Rockstar Games\\Launcher\\launcher" + log_file_append + ".log") if LOG_SENSITIVE_DATA: log.debug("ROCKSTAR_LOG_LOCATION: Checking the file " + log_file + "...") else: log.debug("ROCKSTAR_LOG_LOCATION: Checking the file ***...") # The path to the Launcher log file # likely contains the user's PC profile name (C:\Users\[Name]\Documents...). owned_title_ids = await self.parse_log_file(log_file, owned_title_ids, online_check_success) break except NoGamesInLogException: log.warning("ROCKSTAR_LOG_WARNING: There are no owned games listed in " + str(log_file) + ". Moving to " "the next log file...") current_log_count += 1 except NoLogFoundException: log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " "from. Assuming that the online list is correct...") break except Exception: # This occurs after ROCKSTAR_LOG_ERROR. break if current_log_count == 10: log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read " "from. Assuming that the online list is correct...") for title_id in owned_title_ids: game = self.create_game_from_title_id(title_id) if game not in self.owned_games_cache: log.debug("ROCKSTAR_ADD_GAME: Adding " + title_id + " to owned games cache...") self.owned_games_cache.append(game) return self.owned_games_cache if IS_WINDOWS: async def get_local_size(self, game_id: str, context: Any) -> Optional[int]: title_id = get_game_title_id_from_ros_title_id(game_id) game_status = self.check_game_status(title_id) if game_status.local_game_state == LocalGameState.None_: return 0 return await self._local_client.get_game_size_in_bytes(title_id) @staticmethod async def parse_log_file(log_file, owned_title_ids, online_check_success): owned_title_ids_ = owned_title_ids checked_games_count = 0 total_games_count = len(games_cache) - 1 # We need to subtract 1 to account for the Launcher. if os.path.exists(log_file): with FileReadBackwards(log_file, encoding="utf-8") as frb: while checked_games_count < total_games_count: try: line = frb.readline() except UnicodeDecodeError: log.warning("ROCKSTAR_LOG_UNICODE_WARNING: An invalid Unicode character was found in the line " + line + ". Continuing to next line...") continue except Exception as e: log.error("ROCKSTAR_LOG_ERROR: Reading " + line + " from the log file resulted in the " "exception " + repr(e) + " being thrown. Using the online list... (Please report " "this issue on the plugin's GitHub page!)") raise if not line: log.error("ROCKSTAR_LOG_FINISHED_ERROR: The entire log file was read, but all of the games " "could not be accounted for. Proceeding to import the games that have been " "confirmed...") raise NoGamesInLogException() # We need to do two main things with the log file: # 1. If a game is present in owned_title_ids but not owned according to the log file, then it is # assumed to be a non-Launcher game, and is removed from the list. # 2. If a game is owned according to the log file but is not already present in owned_title_ids, # then it is assumed that the user has purchased the game on the Launcher, but has not yet played # it. In this case, the game will be added to owned_title_ids. if ("launcher" not in line) and ("on branch " in line): # Found a game! # Each log line for a title branch report describes the title id of the game starting at # character 65. Interestingly, the lines all have the same colon as character 75. This implies # that this format was intentionally done by Rockstar, so they likely will not change it anytime # soon. title_id = line[65:75].strip() log.debug("ROCKSTAR_LOG_GAME: The game with title ID " + title_id + " is owned!") if title_id not in owned_title_ids_: if online_check_success is True: # Case 2: The game is owned, but has not been played. log.warning("ROCKSTAR_UNPLAYED_GAME: The game with title ID " + title_id + " is owned, but it has never been played!") owned_title_ids_.append(title_id) checked_games_count += 1 elif "no branches!" in line: title_id = line[65:75].strip() if title_id in owned_title_ids_: # Case 1: The game is not actually owned on the launcher. log.warning("ROCKSTAR_FAKE_GAME: The game with title ID " + title_id + " is not owned on " "the Rockstar Games Launcher!") owned_title_ids_.remove(title_id) checked_games_count += 1 if checked_games_count == total_games_count: break return owned_title_ids_ else: raise NoLogFoundException() async def get_game_time(self, game_id, context): # Although the Rockstar Games Launcher does track the played time for each game, there is currently no known # method for accessing this information. As such, game time will be recorded when games are launched through the # Galaxy 2.0 client. title_id = get_game_title_id_from_ros_title_id(game_id) if title_id in self.running_games_info_list: # The game is running (or has been running). start_time = self.running_games_info_list[title_id].get_start_time() self.running_games_info_list[title_id].update_start_time() current_time = datetime.datetime.now().timestamp() minutes_passed = (current_time - start_time) / 60 if not self.running_games_info_list[title_id].get_pid(): # The PID has been set to None, which means that the game has exited (see self.check_game_status). Now # that the start time is recorded, the game can be safely removed from the list of running games. del self.running_games_info_list[title_id] if self.game_time_cache[title_id]['time_played']: # The game has been played before, so the time will need to be added to the existing cached time. total_time_played = self.game_time_cache[title_id]['time_played'] + minutes_passed self.game_time_cache[title_id]['time_played'] = total_time_played self.game_time_cache[title_id]['last_played'] = current_time return GameTime(game_id=game_id, time_played=int(total_time_played), last_played_time=int(current_time)) else: # The game has not been played before, so a new entry in the game_time_cache dictionary must be made. self.game_time_cache[title_id] = { 'time_played': minutes_passed, 'last_played': current_time } return GameTime(game_id=game_id, time_played=int(minutes_passed), last_played_time=int(current_time)) else: # The game is no longer running (and there is no relevant entry in self.running_games_info_list). if title_id not in self.game_time_cache: self.game_time_cache[title_id] = { 'time_played': None, 'last_played': None } return GameTime(game_id=game_id, time_played=self.game_time_cache[title_id]['time_played'], last_played_time=self.game_time_cache[title_id]['last_played']) def game_times_import_complete(self): log.debug("ROCKSTAR_GAME_TIME: Pushing the cache of played game times to the persistent cache...") self.persistent_cache['game_time_cache'] = pickle.dumps(self.game_time_cache).hex() self.push_cache() def get_friend_user_name_from_user_id(self, user_id): for friend in self.friends_cache: if friend.user_id == user_id: return friend.user_name return None async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any: if CONFIG_OPTIONS['user_presence_mode'] == 2 or CONFIG_OPTIONS['user_presence_mode'] == 3: game = "gtav" if CONFIG_OPTIONS['user_presence_mode'] == 2 else "rdr2" return await self._http_client.get_json_from_request_strict("https://scapi.rockstargames.com/friends/" f"getFriendsWhoPlay?title={game}&platform=pc") return None async def get_user_presence(self, user_id, context): # For user presence settings 2 and 3, we need to verify that the specified user owns the game to get their # stats. friend_name = self.get_friend_user_name_from_user_id(user_id) if LOG_SENSITIVE_DATA: log.debug(f"ROCKSTAR_PRESENCE_START: Getting user presence for {friend_name} (Rockstar ID: {user_id})...") if context: for player in context['onlineFriends']: if player['userId'] == user_id: # This user owns the specified game, so we can return this information. break else: # The user does not own the specified game, so we need to return their last played game. return await self._http_client.get_last_played_game(friend_name) if CONFIG_OPTIONS['user_presence_mode'] == 0: self.presence_cache[user_id] = UserPresence(presence_state=PresenceState.Unknown) # 0 - Disable User Presence else: switch = { 1: self._http_client.get_last_played_game(friend_name), # 1 - Get Last Played Game 2: self._http_client.get_gta_online_stats(user_id, friend_name), # 2 - Get GTA Online Character Stats 3: self._http_client.get_rdo_stats(user_id, friend_name) # 3 - Get Red Dead Online Character Stats } self.presence_cache[user_id] = await asyncio.create_task(switch[CONFIG_OPTIONS['user_presence_mode']]) return self.presence_cache[user_id] async def open_rockstar_browser(self): # This method allows the user to install the Rockstar Games Launcher, if it is not already installed. url = "https://www.rockstargames.com/downloads" log.info(f"Opening Rockstar website {url}") webbrowser.open(url) def check_game_status(self, title_id): state = LocalGameState.None_ game_installed = self._local_client.get_path_to_game(title_id) if game_installed: state |= LocalGameState.Installed if (title_id in self.running_games_info_list and check_if_process_exists(self.running_games_info_list[title_id].get_pid())): state |= LocalGameState.Running elif title_id in self.running_games_info_list: # We will leave the info in the list, because it still contains the game start time for game time # tracking. However, we will set the PID to None to indicate that the game has been closed. self.running_games_info_list[title_id].clear_pid() return LocalGame(str(self.games_cache[title_id]["rosTitleId"]), state) if IS_WINDOWS: async def get_local_games(self): # Since the API requires that get_local_games returns a list of LocalGame objects, local_list is the value # that needs to be returned. However, for internal use (the self.local_games_cache field), the dictionary # local_games is used for greater flexibility. local_games = {} local_list = [] for game in self.total_games_cache: title_id = get_game_title_id_from_ros_title_id(str(game.game_id)) local_game = self.check_game_status(title_id) local_games[title_id] = local_game local_list.append(local_game) self.local_games_cache = local_games log.debug(f"ROCKSTAR_INSTALLED_GAMES: {local_games}") return local_list async def check_for_new_games(self): self.checking_for_new_games = True # The Social Club prevents the user from making too many requests in a given time span to prevent a denial of # service attack. As such, we need to limit online checking to every 5 minutes. For Windows devices, log file # checks will still occur every minute, but for other users, checking games only happens every 5 minutes. owned_title_ids = None online_check_success = False if not self.last_online_game_check or time() >= self.last_online_game_check + 300: owned_title_ids, online_check_success = await self.get_owned_games_online() elif IS_WINDOWS: log.debug("ROCKSTAR_SC_ONLINE_GAMES_SKIP: No attempt has been made to scrape the user's games from the " "Social Club, as it has not been 5 minutes since the last check.") await self.get_owned_games(owned_title_ids, online_check_success) await asyncio.sleep(60 if IS_WINDOWS else 300) self.checking_for_new_games = False async def check_game_statuses(self): self.updating_game_statuses = True for title_id, current_local_game in self.local_games_cache.items(): new_local_game = self.check_game_status(title_id) if new_local_game != current_local_game: log.debug(f"ROCKSTAR_LOCAL_CHANGE: The status for {title_id} has changed from: {current_local_game} to " f"{new_local_game}.") self.update_local_game_status(new_local_game) self.local_games_cache[title_id] = new_local_game await asyncio.sleep(5) self.updating_game_statuses = False def list_running_game_pids(self): info_list = [] for key, value in self.running_games_info_list.items(): info_list.append(value.get_pid()) return str(info_list) if IS_WINDOWS: async def launch_platform_client(self): if not self._local_client.get_local_launcher_path(): await self.open_rockstar_browser() return pid = await self._local_client.launch_game_from_title_id("launcher") if not pid: log.warning("ROCKSTAR_LAUNCHER_FAILED: The Rockstar Games Launcher could not be launched!") if IS_WINDOWS: async def shutdown_platform_client(self): if not self._local_client.get_local_launcher_path(): await self.open_rockstar_browser() return await self._local_client.kill_launcher() if IS_WINDOWS: async def launch_game(self, game_id): if not self._local_client.get_local_launcher_path(): await self.open_rockstar_browser() return title_id = get_game_title_id_from_ros_title_id(game_id) game_pid = await self._local_client.launch_game_from_title_id(title_id) if game_pid: self.running_games_info_list[title_id] = RunningGameInfo() self.running_games_info_list[title_id].set_info(game_pid) log.debug(f"ROCKSTAR_PIDS: {self.list_running_game_pids()}") local_game = LocalGame(game_id, LocalGameState.Running | LocalGameState.Installed) self.update_local_game_status(local_game) self.local_games_cache[title_id] = local_game else: log.error(f'cannot start game: {title_id}') if IS_WINDOWS: async def install_game(self, game_id): if not self._local_client.get_local_launcher_path(): await self.open_rockstar_browser() return title_id = get_game_title_id_from_ros_title_id(game_id) log.debug("ROCKSTAR_INSTALL_REQUEST: Requesting to install " + title_id + "...") # There is no need to check if the game is a pre-order, since the InstallLocation registry key will be # unavailable if it is. self._local_client.install_game_from_title_id(title_id) if IS_WINDOWS: async def uninstall_game(self, game_id): if not self._local_client.get_local_launcher_path(): await self.open_rockstar_browser() return title_id = get_game_title_id_from_ros_title_id(game_id) log.debug("ROCKSTAR_UNINSTALL_REQUEST: Requesting to uninstall " + title_id + "...") self._local_client.uninstall_game_from_title_id(title_id) def create_game_from_title_id(self, title_id): return Game(str(self.games_cache[title_id]["rosTitleId"]), self.games_cache[title_id]["friendlyName"], None, self.games_cache[title_id]["licenseInfo"]) def tick(self): if not self.is_authenticated(): return if not self.checking_for_new_games: log.debug("Checking for new games...") asyncio.create_task(self.check_for_new_games()) if not self.updating_game_statuses and IS_WINDOWS: log.debug("Checking local game statuses...") asyncio.create_task(self.check_game_statuses())
class UplayPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Uplay, __version__, reader, writer, token) self.client = BackendClient(self) self.local_client = LocalClient() self.cached_game_statuses = {} self.games_collection = GamesCollection() self.process_watcher = ProcessWatcher() self.game_status_notifier = GameStatusNotifier(self.process_watcher) self.tick_count = 0 self.updating_games = False self.owned_games_sent = False self.parsing_club_games = False def auth_lost(self): self.lost_authentication() async def authenticate(self, stored_credentials=None): if not stored_credentials: return NextStep("web_session", AUTH_PARAMS, cookies=COOKIES) else: try: user_data = await self.client.authorise_with_stored_credentials( stored_credentials) except (AccessDenied, AuthenticationRequired) as e: log.exception(repr(e)) raise InvalidCredentials() except Exception as e: log.exception(repr(e)) raise e else: self.local_client.initialize(user_data['userId']) self.client.set_auth_lost_callback(self.auth_lost) return Authentication(user_data['userId'], user_data['username']) async def pass_login_credentials(self, step, credentials, cookies): """Called just after CEF authentication (called as NextStep by authenticate)""" user_data = await self.client.authorise_with_cookies(cookies) self.local_client.initialize(user_data['userId']) self.client.set_auth_lost_callback(self.auth_lost) return Authentication(user_data['userId'], user_data['username']) async def get_owned_games(self): if not self.client.is_authenticated(): raise AuthenticationRequired() self._parse_local_games() self._parse_local_game_ownership() await self._parse_club_games() self.owned_games_sent = True for game in self.games_collection: game.considered_for_sending = True return [ game.as_galaxy_game() for game in self.games_collection if game.owned ] async def _parse_club_games(self): if not self.parsing_club_games: try: self.parsing_club_games = True games = await self.client.get_club_titles() club_games = [] for game in games: if "platform" in game: if game["platform"] == "PC": log.info( f"Parsed game from Club Request {game['title']}" ) club_games.append( UbisoftGame(space_id=game['spaceId'], launch_id='', third_party_id='', name=game['title'], path='', type=GameType.New, special_registry_path='', exe='', status=GameStatus.Unknown, owned=True)) self.games_collection.append(club_games) except ApplicationError as e: log.error( f"Encountered exception while parsing club games {repr(e)}" ) raise e except Exception as e: log.error( f"Encountered exception while parsing club games {repr(e)}" ) finally: self.parsing_club_games = False else: # Wait until club games get parsed if parsing is already in progress while self.parsing_club_games: await asyncio.sleep(0.2) def _parse_local_games(self): """Parsing local files should lead to every game having a launch id. A game in the games_collection which doesn't have a launch id probably means that a game was added through the get_club_titles request but its space id was not present in configuration file and we couldn't find a matching launch id for it.""" if self.local_client.configurations_accessible(): configuration_data = self.local_client.read_config() p = LocalParser() games = [] for game in p.parse_games(configuration_data): games.append(game) self.games_collection.append(games) def _parse_local_game_ownership(self): if self.local_client.ownership_accesible(): ownership_data = self.local_client.read_ownership() p = LocalParser() ownership_records = p.get_owned_local_games(ownership_data) log.info(f" Ownership Records {ownership_records}") for game in self.games_collection: if game.launch_id: if int(game.launch_id) in ownership_records: game.owned = True def _update_games(self): self.updating_games = True self._parse_local_games() self.updating_games = False def _update_local_games_status(self): cached_statuses = self.cached_game_statuses if cached_statuses is None: return for game in self.games_collection: if game.launch_id in cached_statuses: self.game_status_notifier.update_game(game) if game.status != cached_statuses[game.launch_id]: log.info( f"Game {game.name} path changed: updating status from {cached_statuses[game.launch_id]} to {game.status}" ) self.update_local_game_status(game.as_local_game()) self.cached_game_statuses[game.launch_id] = game.status else: self.game_status_notifier.update_game(game) ''' If a game wasn't previously in a cache then and it appears with an installed or running status it most likely means that client was just installed ''' if game.status in [GameStatus.Installed, GameStatus.Running]: self.update_local_game_status(game.as_local_game()) self.cached_game_statuses[game.launch_id] = game.status async def get_local_games(self): self._parse_local_games() local_games = [] for game in self.games_collection: self.cached_game_statuses[game.launch_id] = game.status if game.status == GameStatus.Installed or game.status == GameStatus.Running: local_games.append(game.as_local_game()) self._update_local_games_status() return local_games async def _add_new_games(self, games): await self._parse_club_games() self._parse_local_game_ownership() for game in games: if game.owned: self.add_game(game.as_galaxy_game()) async def get_game_times(self): if not self.client.is_authenticated(): raise AuthenticationRequired() game_times = [] games_with_space = [ game for game in self.games_collection if game.space_id ] try: tasks = [ self.client.get_game_stats(game.space_id) for game in games_with_space ] stats = await asyncio.gather(*tasks) for st, game in zip(stats, games_with_space): statscards = st.get('Statscards', None) if statscards is None: continue playtime, last_played = find_playtime(statscards, default_total_time=0, default_last_played=0) log.info( f'Stats for {game.name}: playtime: {playtime}, last_played: {last_played}' ) if playtime is not None and last_played is not None: game_times.append( GameTime(game.space_id, playtime, last_played)) except ApplicationError as e: log.exception("Game times:" + repr(e)) raise e except Exception as e: log.exception("Game times:" + repr(e)) finally: return game_times async def get_unlocked_challenges(self, game_id): """Challenges are a unique uplay club feature and don't directly translate to achievements""" if not self.client.is_authenticated(): raise AuthenticationRequired() for game in self.games_collection: if game.space_id == game_id or game.launch_id == game_id: if not game.space_id: return [] challenges = await self.client.get_challenges(game.space_id) return [ Achievement(achievement_id=challenge["id"], achievement_name=challenge["name"], unlock_time=int( datetime.datetime.timestamp( dateutil.parser.parse( challenge["completionDate"])))) for challenge in challenges["actions"] if challenge["isCompleted"] and not challenge["isBadge"] ] async def launch_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection.get_local_games(): if (game.space_id == game_id or game.launch_id == game_id) and game.status == GameStatus.Installed: if game.type == GameType.Steam: if is_steam_installed(): url = f"start steam://rungameid/{game.third_party_id}" else: url = f"start uplay://open/game/{game.launch_id}" elif game.type == GameType.New or game.type == GameType.Legacy: url = f"start uplay://launch/{game.launch_id}" else: log.error(f"Unsupported game type {game.name}") self.open_uplay_client() return log.info(f"Launching game '{game.name}' by protocol: [{url}]") subprocess.Popen(url, shell=True) return log.info("Failed to launch game, launching client instead.") self.open_uplay_client() async def install_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection: if (game.space_id == game_id or game.launch_id == game_id) and game.status in [ GameStatus.NotInstalled, GameStatus.Unknown ]: if game.launch_id: log.info( f"Found game with game_id: {game_id}, {game.launch_id}" ) subprocess.Popen(f"start uplay://install/{game.launch_id}", shell=True) return # if launch_id is not known, try to launch local client instead self.open_uplay_client() log.info( f"Did not found game with game_id: {game_id}, proper launch_id and NotInstalled status, launching client." ) async def uninstall_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection.get_local_games(): if (game.space_id == game_id or game.launch_id == game_id) and game.status == GameStatus.Installed: subprocess.Popen(f"start uplay://uninstall/{game.launch_id}", shell=True) return self.open_uplay_client() log.info( f"Did not found game with game_id: {game_id}, proper launch_id and Installed status, launching client." ) def user_can_perform_actions(self): if not self.local_client.is_installed: self.open_uplay_browser() return False if not self.local_client.was_user_logged_in: self.open_uplay_client() return False return True def open_uplay_client(self): subprocess.Popen(f"start uplay://", shell=True) def open_uplay_browser(self): url = f'https://uplay.ubisoft.com' log.info(f"Opening uplay website: {url}") webbrowser.open(url, autoraise=True) def refresh_game_statuses(self): if not self.local_client.was_user_logged_in: return statuses = self.game_status_notifier.statuses new_games = [] for game in self.games_collection: if game.launch_id in statuses: if statuses[ game. launch_id] == GameStatus.Installed and game.status != GameStatus.Installed: log.info(f"updating status for {game.name} to installed") game.status = GameStatus.Installed self.update_local_game_status(game.as_local_game()) elif statuses[ game. launch_id] == GameStatus.Running and game.status != GameStatus.Running: log.info(f"updating status for {game.name} to running") game.status = GameStatus.Running self.update_local_game_status(game.as_local_game()) elif statuses[game.launch_id] in [ GameStatus.NotInstalled, GameStatus.Unknown ] and game.status not in [ GameStatus.NotInstalled, GameStatus.Unknown ]: log.info( f"updating status for {game.name} to not installed") game.status = GameStatus.NotInstalled self.update_local_game_status(game.as_local_game()) if self.owned_games_sent and not game.considered_for_sending: game.considered_for_sending = True new_games.append(game) if new_games: asyncio.create_task(self._add_new_games(new_games)) async def get_friends(self): friends = await self.client.get_friends() return [ FriendInfo(user_id=friend["pid"], user_name=friend["nameOnPlatform"]) for friend in friends["friends"] ] def tick(self): loop = asyncio.get_event_loop() if SYSTEM == System.WINDOWS: self.tick_count += 1 if self.tick_count % 1 == 0: self.refresh_game_statuses() if self.tick_count % 5 == 0: self.game_status_notifier.launcher_log_path = self.local_client.launcher_log_path if self.tick_count % 9 == 0: self._update_local_games_status() if self.local_client.ownership_changed(): if not self.updating_games: log.info( 'Ownership file has been changed or created. Reparsing.' ) loop.run_in_executor(None, self._update_games) return def shutdown(self): log.info("Plugin shutdown.") asyncio.create_task(self.client.close())
class MinecraftPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Minecraft, __version__, reader, writer, token) self.local_client = LocalClient() self.minecraft_launcher = None self.minecraft_uninstall_command = None self.minecraft_installation_status = LocalGameState.None_ self.minecraft_running_check = None self.tick_count = 0 async def authenticate(self, stored_credentials=None): self.store_credentials({'dummy': 'dummy'}) return Authentication(user_id='Minecraft_ID', user_name='Minecraft Player') async def get_owned_games(self): return [ Game('1', 'Minecraft', None, LicenseInfo(LicenseType.SinglePurchase)) ] async def get_local_games(self): self.minecraft_launcher = self.local_client.get_minecraft_launcher_path( ) if self.minecraft_launcher: return [LocalGame('1', LocalGameState.Installed)] return [] async def install_game(self, game_id): if sys.platform == 'win32': r = requests.get( "https://launcher.mojang.com/download/MinecraftInstaller.msi") installer_path = os.path.join(tempfile.gettempdir(), 'MinecraftInstaller.msi') open(installer_path, 'wb').write(r.content) subprocess.Popen(installer_path, shell=True) else: r = requests.get( "https://launcher.mojang.com/download/Minecraft.dmg") installer_path = os.path.join(tempfile.gettempdir(), 'Minecraft.dmg') open(installer_path, 'wb').write(r.content) subprocess.Popen("open " + installer_path, shell=True) async def launch_game(self, game_id): if sys.platform == 'win32': cmd = f'"{self.minecraft_launcher}"' log.info(f"Launching minecraft by command {cmd}") subprocess.Popen(cmd) else: cmd = f"open {self.minecraft_launcher}" log.info(f"Launching minecraft by command {cmd}") subprocess.Popen(cmd, shell=True) async def uninstall_game(self, game_id): if sys.platform == 'win32': if not self.minecraft_uninstall_command: self.minecraft_uninstall_command = self.local_client.find_minecraft_uninstall_command( ) if self.minecraft_uninstall_command: log.info(f"calling {self.minecraft_uninstall_command}") subprocess.Popen(f'{self.minecraft_uninstall_command}') else: log.error("Unable to find minecraft uninstall command") else: shutil.rmtree(self.minecraft_launcher) def tick(self): potential_path = self.local_client.get_minecraft_launcher_path() if potential_path and not self.minecraft_launcher: self.minecraft_launcher = potential_path self.update_local_game_status( LocalGame('1', LocalGameState.Installed)) self.minecraft_uninstall_command = self.local_client.find_minecraft_uninstall_command( ) elif not potential_path and self.minecraft_launcher: self.minecraft_launcher = None self.update_local_game_status(LocalGame('1', LocalGameState.None_)) self.minecraft_uninstall_command = None self.tick_count += 1 if self.local_client.running_process and ( not self.minecraft_running_check or self.minecraft_running_check.done()): self.local_client.is_minecraft_still_running() elif self.tick_count % 5 == 0: if self.minecraft_launcher and ( not self.minecraft_running_check or self.minecraft_running_check.done()): self.minecraft_running_check = asyncio.create_task( self.local_client.was_minecraft_launched()) if self.minecraft_launcher and self.local_client.running_process and self.minecraft_installation_status != LocalGameState.Installed | LocalGameState.Running: self.minecraft_installation_status = LocalGameState.Installed | LocalGameState.Running self.update_local_game_status( LocalGame('1', LocalGameState.Installed | LocalGameState.Running)) if self.minecraft_launcher and not self.local_client.running_process and self.minecraft_installation_status == LocalGameState.Installed | LocalGameState.Running: self.minecraft_installation_status = LocalGameState.Installed self.update_local_game_status( LocalGame('1', LocalGameState.Installed)) def shutdown(self): pass
def __init__(self, reader, writer, token): super().__init__(Platform(manifest['platform']), manifest['version'], reader, writer, token) self._api = ApiClient(self.store_credentials, self.lost_authentication) self._local = LocalClient()
class BethesdaPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Bethesda, __version__, reader, writer, token) self._http_client = AuthenticatedHttpClient(self.store_credentials) self.bethesda_client = BethesdaClient(self._http_client) self.local_client = LocalClient() self.products_cache = product_cache self.owned_games_cache = None self._asked_for_local = False self.update_game_running_status_task = None self.update_game_installation_status_task = None self.check_for_new_games_task = None self.running_games = {} self.launching_lock = None async def authenticate(self, stored_credentials=None): if not stored_credentials: return NextStep( "web_session", AUTH_PARAMS, cookies=[Cookie("passedICO", "true", ".bethesda.net")]) try: log.info("Got stored credentials") cookies = pickle.loads( bytes.fromhex(stored_credentials['cookie_jar'])) cookies_parsed = [] for cookie in cookies: if cookie.key in cookies_parsed and cookie.domain: self._http_client.update_cookies( {cookie.key: cookie.value}) elif cookie.key not in cookies_parsed: self._http_client.update_cookies( {cookie.key: cookie.value}) cookies_parsed.append(cookie.key) log.info("Finished parsing stored credentials, authenticating") user = await self._http_client.authenticate() return Authentication(user_id=user['user_id'], user_name=user['display_name']) except Exception as e: log.error( f"Couldn't authenticate with stored credentials {repr(e)}") raise InvalidCredentials() async def pass_login_credentials(self, step, credentials, cookies): cookiez = {} for cookie in cookies: cookiez[cookie['name']] = cookie['value'] self._http_client.update_cookies(cookiez) try: user = await self._http_client.authenticate() except Exception as e: log.error(repr(e)) raise InvalidCredentials() return Authentication(user_id=user['user_id'], user_name=user['display_name']) async def get_owned_games(self): owned_ids = [] matched_ids = [] games_to_send = [] pre_orders = [] try: owned_ids = await self.bethesda_client.get_owned_ids() except UnknownError as e: log.warning(f"No owned games detected {repr(e)}") log.info(f"Owned Ids: {owned_ids}") if owned_ids: for entitlement_id in owned_ids: for product in self.products_cache: if 'reference_id' in self.products_cache[product]: for reference_id in self.products_cache[product][ 'reference_id']: if entitlement_id in reference_id: self.products_cache[product]['owned'] = True matched_ids.append(entitlement_id) pre_orders = set(owned_ids) - set(matched_ids) for pre_order in pre_orders: pre_order_details = await self.bethesda_client.get_game_details( pre_order) if pre_order_details and 'Entry' in pre_order_details: for entry in pre_order_details['Entry']: if 'fields' in entry and 'productName' in entry['fields']: if entry['fields'][ 'productName'] in self.products_cache: self.products_cache[ entry['fields']['productName']]['owned'] = True else: games_to_send.append( Game( pre_order, entry['fields']['productName'] + " (Pre Order)", None, LicenseInfo(LicenseType.SinglePurchase))) break for product in self.products_cache: if self.products_cache[product]["owned"] and self.products_cache[ product]["free_to_play"]: games_to_send.append( Game(self.products_cache[product]['local_id'], product, None, LicenseInfo(LicenseType.FreeToPlay))) elif self.products_cache[product]["owned"]: games_to_send.append( Game(self.products_cache[product]['local_id'], product, None, LicenseInfo(LicenseType.SinglePurchase))) log.info(f"Games to send (with free games): {games_to_send}") self.owned_games_cache = games_to_send return games_to_send async def get_local_games(self): local_games = [] installed_products = self.local_client.get_installed_games( self.products_cache) log.info(f"Installed products {installed_products}") for product in self.products_cache: for installed_product in installed_products: if installed_products[ installed_product] == self.products_cache[product][ 'local_id']: self.products_cache[product]['installed'] = True local_games.append( LocalGame(installed_products[installed_product], LocalGameState.Installed)) self._asked_for_local = True log.info(f"Returning local games {local_games}") return local_games async def install_game(self, game_id): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return if not self.local_client.is_installed: await self._open_betty_browser() return if self.local_client.is_running: self.local_client.focus_client_window() await self.launch_game(game_id) else: uuid = None for product in self.products_cache: if self.products_cache[product]['local_id'] == game_id: if self.products_cache[product]['installed']: log.warning( "Got install on already installed game, launching") return await self.launch_game(game_id) uuid = "\"" + self.products_cache[product]['uuid'] + "\"" cmd = "\"" + self.local_client.client_exe_path + "\"" + f" --installproduct={uuid}" log.info(f"Calling install game with command {cmd}") subprocess.Popen(cmd, shell=True) async def launch_game(self, game_id): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return if not self.local_client.is_installed: await self._open_betty_browser() return for product in self.products_cache: if self.products_cache[product]['local_id'] == game_id: if not self.products_cache[product]['installed']: if not self.local_client.is_running: log.warning( "Got launch on a not installed game, installing") return await self.install_game(game_id) else: if not self.local_client.is_running: self.launching_lock = time.time() + 45 else: self.launching_lock = time.time() + 30 self.running_games[game_id] = None self.update_local_game_status( LocalGame( game_id, LocalGameState.Installed | LocalGameState.Running)) self.update_game_running_status_task.cancel() log.info(f"Calling launch command for id {game_id}") cmd = f"start bethesdanet://run/{game_id}" subprocess.Popen(cmd, shell=True) async def uninstall_game(self, game_id): if sys.platform != 'win32': log.error(f"Incompatible platform {sys.platform}") return if not self.local_client.is_installed: await self._open_betty_browser() return for product in self.products_cache: if self.products_cache[product]['local_id'] == game_id: if not self.products_cache[product]['installed']: return log.info(f"Calling uninstall command for id {game_id}") cmd = f"start bethesdanet://uninstall/{game_id}" subprocess.Popen(cmd, shell=True) if self.local_client.is_running: await asyncio.sleep( 2) # QOL, bethesda slowly reacts to uninstall command, self.local_client.focus_client_window() async def _open_betty_browser(self): url = "https://bethesda.net/game/bethesda-launcher" log.info(f"Opening Bethesda website on url {url}") webbrowser.open(url) async def _heavy_installation_status_check(self): installed_products = self.local_client.get_installed_games( self.products_cache) products_cache_installed_products = {} for product in self.products_cache: if self.products_cache[product]['installed']: products_cache_installed_products[ product] = self.products_cache[product]['local_id'] for installed_product in installed_products: if installed_product not in products_cache_installed_products: self.products_cache[installed_product]["installed"] = True self.update_local_game_status( LocalGame(installed_products[installed_product], LocalGameState.Installed)) for installed_product in products_cache_installed_products: if installed_product not in installed_products: self.products_cache[installed_product]["installed"] = False self.update_local_game_status( LocalGame( products_cache_installed_products[installed_product], LocalGameState.None_)) def _light_installation_status_check(self): for local_game in self.local_client.local_games_cache: local_game_installed = self.local_client.is_local_game_installed( self.local_client.local_games_cache[local_game]) if local_game_installed and not self.products_cache[local_game][ "installed"]: self.products_cache[local_game]["installed"] = True self.update_local_game_status( LocalGame( self.local_client.local_games_cache[local_game] ['local_id'], LocalGameState.Installed)) elif not local_game_installed and self.products_cache[local_game][ "installed"]: self.products_cache[local_game]["installed"] = False self.update_local_game_status( LocalGame( self.local_client.local_games_cache[local_game] ['local_id'], LocalGameState.None_)) async def update_game_installation_status(self): if self.local_client.clientgame_changed(): await asyncio.sleep(1) await self._heavy_installation_status_check() else: self._light_installation_status_check() async def update_game_running_status(self): process_iter_interval = 0.10 dont_downgrade_status = False if self.launching_lock and self.launching_lock >= time.time(): dont_downgrade_status = True process_iter_interval = 0.01 for running_game in self.running_games.copy(): if not self.running_games[running_game] and dont_downgrade_status: log.info(f"Found 'just launched' game {running_game}") continue elif not self.running_games[running_game]: log.info( f"Found 'just launched' game but its still without pid and its time run out {running_game}" ) self.running_games.pop(running_game) self.update_local_game_status( LocalGame(running_game, LocalGameState.Installed)) continue if self.running_games[running_game].is_running(): return self.running_games.pop(running_game) self.update_local_game_status( LocalGame(running_game, LocalGameState.Installed)) for process in psutil.process_iter(attrs=['name'], ad_value=''): await asyncio.sleep(process_iter_interval) for local_game in self.local_client.local_games_cache: try: if process.name().lower( ) in self.local_client.local_games_cache[local_game][ 'execs']: log.info(f"Found a running game! {local_game}") local_id = self.local_client.local_games_cache[ local_game]['local_id'] if local_id not in self.running_games: self.update_local_game_status( LocalGame( local_id, LocalGameState.Installed | LocalGameState.Running)) self.running_games[local_id] = process return except (psutil.AccessDenied, psutil.NoSuchProcess): break await asyncio.sleep(3) async def check_for_new_games(self): owned_games = await self.get_owned_games() for owned_game in owned_games: if owned_game not in self.owned_games_cache: self.add_game(owned_game) self.owned_games_cache = owned_games await asyncio.sleep(60) def tick(self): if self._asked_for_local and ( not self.update_game_installation_status_task or self.update_game_installation_status_task.done()): self.update_game_installation_status_task = asyncio.create_task( self.update_game_installation_status()) if self._asked_for_local and ( not self.update_game_running_status_task or self.update_game_running_status_task.done()): self.update_game_running_status_task = asyncio.create_task( self.update_game_running_status()) if self.owned_games_cache and (not self.check_for_new_games_task or self.check_for_new_games_task.done()): self.check_for_new_games_task = asyncio.create_task( self.check_for_new_games()) def shutdown(self): asyncio.create_task(self._http_client.close())