Ejemplo n.º 1
0
 def __init__(self, reader, writer, token):
     super().__init__(Platform.NintendoGameCube, __version__, reader,
                      writer, token)
     self.backend_client = BackendClient()
     self.games = []
     self.game_times = get_the_game_times()
     self.local_games_cache = self.local_games_list()
Ejemplo n.º 2
0
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Battlenet, version, reader, writer, token)
        self.local_client = LocalClient(self._update_statuses)
        self.authentication_client = AuthenticatedHttpClient(self)
        self.backend_client = BackendClient(self, self.authentication_client)

        self.watched_running_games = set()
Ejemplo n.º 3
0
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Battlenet, version, reader, writer, token)

        log.info(f"Starting Battle.net plugin, version {version}")

        self.bnet_client = None
        self.local_client = LocalClient()
        self.authentication_client = AuthenticatedHttpClient(self)
        self.backend_client = BackendClient(self, self.authentication_client)
        self.social_features = SocialFeatures(self.authentication_client)
        self.error_state = False

        self.running_task = None

        self.database_parser = None
        self.config_parser = None
        self.uninstaller = None

        self.owned_games_cache = []

        self._classic_games_thread = None
        self._battlenet_games_thread = None
        self._installed_battlenet_games = {}
        self._installed_battlenet_games_lock = Lock()

        self.installed_games = self._parse_local_data()
        self.watched_running_games = set()

        self.notifications_enabled = False
        loop = asyncio.get_event_loop()
        loop.create_task(self._register_local_data_watcher())
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Battlenet, version, reader, writer, token)
        self.local_client = LocalClient(self._update_statuses)
        self.authentication_client = AuthenticatedHttpClient(self)
        self.backend_client = BackendClient(self, self.authentication_client)
        self.social_features = SocialFeatures(self.authentication_client)

        self.owned_games_cache = []
        self.watched_running_games = set()
        self.local_games_called = False
Ejemplo n.º 5
0
    def __init__(self, reader, writer, token):
        super().__init__(Platform.SuperNintendoEntertainmentSystem, __version__, reader, writer, token)
        self.backend_client = BackendClient(self)
        self.games = []
        self.local_games_cache = []
        self.proc = None
        self.running_game_id = ""
        self.tick_count = 0

        self.create_task(self._update_local_games(), "Update local games")
Ejemplo n.º 6
0
 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
Ejemplo n.º 7
0
 def __init__(self, reader, writer, token):
     super().__init__(Platform.ColecoVision, get_version(), reader, writer, token)
     try:
         self.config = Config() # If we can't create a good config, we can't run the plugin.
     except FileNotFoundError:
         self.close()
     else:
         self.backend_client = BackendClient(self.config)
         self.games = []
         self.local_games_cache = self.local_games_list()
         self.process = None
         self.running_game_id = None
 def __init__(self, reader, writer, token):
     super().__init__(Platform.NintendoWii, __version__, reader, writer,
                      token)
     self.backend_client = BackendClient()
     self.games = []
     if not os.path.exists(
             os.path.dirname(os.path.realpath(__file__)) +
             r'\gametimes.xml'):
         copyfile(
             os.path.dirname(os.path.realpath(__file__)) +
             r'\files\gametimes.xml',
             os.path.dirname(os.path.realpath(__file__)) +
             r'\gametimes.xml')
     self.game_times = self.get_the_game_times()
     self.local_games_cache = self.local_games_list()
     self.runningGame = self.runningGame = {
         "game_id": "",
         "starting_time": 0,
         "dolphin_running": None,
         "launched": False
     }
Ejemplo n.º 9
0
class PlayStation2Plugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.PlayStation2, __version__, reader, writer, token)
        self.backend_client = BackendClient()
        self.games = []
        self.local_games_cache = self.local_games_list()

        
    async def authenticate(self, stored_credentials=None):
        return self.do_auth()

        
    async def pass_login_credentials(self, step, credentials, cookies):
        return self.do_auth()


    def do_auth(self):
        user_data = {}
        username = user_config.roms_path
        user_data["username"] = username
        self.store_credentials(user_data)
        return Authentication("pcsx2_user", user_data["username"])


    async def launch_game(self, game_id):
        emu_path = user_config.emu_path
        no_gui = user_config.emu_no_gui
        fullscreen = user_config.emu_fullscreen
        config = user_config.emu_config
        config_folder = user_config.config_path

        for game in self.games:
            if str(game[1]) == game_id:
                rom_file = os.path.splitext(os.path.basename(game[0]))[0]
                config_folder_game = config_folder + "/" + rom_file
                if config and os.path.isdir(config_folder_game):
                    config_arg = '--cfgpath=' + config_folder + "/" + rom_file
                    if no_gui and fullscreen:
                        subprocess.Popen([emu_path, "--nogui", "--fullscreen", config_arg, game[0]])
                        break
                    if no_gui and not fullscreen:
                        subprocess.Popen([emu_path, "--nogui", config_arg, game[0]])
                        break
                    if not no_gui and fullscreen:
                        subprocess.Popen([emu_path, "--fullscreen", config_arg, game[0]])
                        break
                    subprocess.Popen([emu_path, config_arg, game[0]])
                    break
                else:
                    if no_gui and fullscreen:
                        subprocess.Popen([emu_path, "--nogui", "--fullscreen", game[0]])
                        break
                    if no_gui and not fullscreen:
                        subprocess.Popen([emu_path, "--nogui", game[0]])
                        break
                    if not no_gui and fullscreen:
                        subprocess.Popen([emu_path, "--fullscreen", game[0]])
                        break
                    subprocess.Popen([emu_path, game[0]])
                    break
        return

    async def install_game(self, game_id):
        pass

    async def uninstall_game(self, game_id):
        pass


    def local_games_list(self):
        local_games = []
        for game in self.games:
            local_games.append(
                LocalGame(
                    str(game[1]),
                    LocalGameState.Installed
                )
            )
        return local_games


    def tick(self):

        async def update_local_games():
            loop = asyncio.get_running_loop()
            new_local_games_list = await loop.run_in_executor(None, self.local_games_list)
            notify_list = self.backend_client.get_state_changes(self.local_games_cache, new_local_games_list)
            self.local_games_cache = new_local_games_list
            for local_game_notify in notify_list:
                self.update_local_game_status(local_game_notify)

        asyncio.create_task(update_local_games())


    async def get_owned_games(self):
        if(user_config.use_database):
            self.games = self.backend_client.get_games_db()
        else:
            self.games = self.backend_client.get_games_gb()
        owned_games = []
        
        for game in self.games:
            owned_games.append(
                Game(
                    str(game[1]),
                    game[2],
                    None,
                    LicenseInfo(LicenseType.SinglePurchase, None)
                )
            )
            
        return owned_games

    async def get_local_games(self):
        return self.local_games_cache

    def shutdown(self):
        pass
Ejemplo n.º 10
0
class RPCS3Plugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.ColecoVision, get_version(), reader, writer, token)
        try:
            self.config = Config() # If we can't create a good config, we can't run the plugin.
        except FileNotFoundError:
            self.close()
        else:
            self.backend_client = BackendClient(self.config)
            self.games = []
            self.local_games_cache = self.local_games_list()
            self.process = None
            self.running_game_id = None


    async def authenticate(self, stored_credentials=None):
        return self.do_auth()


    async def pass_login_credentials(self, step, credentials, cookies):
        return self.do_auth()


    def do_auth(self):

        username = ''
        with open(self.config.localusername, 'r') as username_file:
            username = username_file.read()

        user_data = {}
        user_data['username'] = username
        self.store_credentials(user_data)
        return Authentication('rpcs3_user', user_data['username'])


    async def launch_game(self, game_id):

        args = []
        eboot_bin = self.config.joinpath(
            self.backend_client.get_game_path(game_id),
            'USRDIR', 
            'EBOOT.BIN')

        if self.config.no_gui:
            args.append('--no-gui')

        command = [self.config.rpcs3_exe, eboot_bin] + args
        self.process = subprocess.Popen(command)
        self.backend_client.start_game_time()
        self.running_game_id = game_id

        return


    # Only as placeholders so the feature is recognized
    async def install_game(self, game_id):
        pass

    async def uninstall_game(self, game_id):
        pass


    async def prepare_game_times_context(self, game_ids):
        return self.get_game_times(game_ids)


    async def prepare_achievements_context(self, game_ids):
        return self.get_trophy_achs()


    async def get_game_time(self, game_id, context):
        game_time = context.get(game_id)
        return game_time


    async def get_unlocked_achievements(self, game_id, context):
        achs = context.get(game_id)
        return achs    


    def get_game_times(self, game_ids):

        # Get the path of the game times file.
        base_path = os.path.dirname(os.path.realpath(__file__))
        game_times_path = os.path.join(base_path, 'game_times.json')
        game_times = {}

        # If the file does not exist, create it with default values.
        if not os.path.exists(game_times_path):
            for game in self.games:

                game_id = str(game[0])
                game_times[game_id] = GameTime(game_id, 0, None)

            with open(game_times_path, 'w', encoding='utf-8') as game_times_file:
                json.dump(game_times, game_times_file, indent=4)

        # If (when) the file exists, read it and return the game times.  
        with open(game_times_path, 'r', encoding='utf-8') as game_times_file:
            game_times_json = json.load(game_times_file)

            for game_id in game_times_json:
                if game_id in game_ids:
                    time_played = game_times_json.get(game_id).get('time_played')
                    last_time_played = game_times_json.get(game_id).get('last_time_played')

                    game_times[game_id] = GameTime(game_id, time_played, last_time_played)

        return game_times

    def get_trophy_achs(self):

        game_ids = []
        for game in self.games:
            game_ids.append(game[0])

        trophies = None
        all_achs = {}
        for game_id in game_ids:
            game_path = self.backend_client.get_game_path(game_id)

            try:
                trophies = Trophy(self.config, game_path)
                keys = trophies.tropusr.table6.keys()

                game_achs = []
                for key in keys:
                    ach = trophies.trop2ach(key)
                    if ach is not None:
                        game_achs.append(trophies.trop2ach(key))

                all_achs[game_id] = game_achs

            # If tropusr doesn't exist, this game has no trophies.
            except AttributeError:
                all_achs[game_id] = []

        return all_achs


    def tick(self):
        try:
            if self.process.poll() is not None:

                self.backend_client.end_game_time()
                self.update_json_game_time(
                    self.running_game_id,
                    self.backend_client.get_session_duration(),
                    int(time.time()))
    
                # Only update recently played games. Updating all game times every second fills up log way too quickly.
                self.create_task(self.update_galaxy_game_times(self.running_game_id), 'Update Galaxy game times')

                self.process = None
                self.running_game_id = None

        except AttributeError:
            pass

        self.create_task(self.update_local_games(), 'Update local games')
        self.create_task(self.update_achievements(), 'Update achievements')


    async def update_local_games(self):
        loop = asyncio.get_running_loop()

        new_list = await loop.run_in_executor(None, self.local_games_list)
        notify_list = self.backend_client.get_state_changes(self.local_games_cache, new_list)
        self.local_games_cache = new_list
        
        for local_game_notify in notify_list:
            self.update_local_game_status(local_game_notify)


    async def update_galaxy_game_times(self, game_id):

        # Leave time for Galaxy to fetch games before updating times
        await asyncio.sleep(60) 
        loop = asyncio.get_running_loop()

        game_times = await loop.run_in_executor(None, self.get_game_times, [game_id])
        for game_id in game_times:
            self.update_game_time(game_times[game_id])


    async def update_achievements(self):

        # Leave time for Galaxy to fetch games before updating times
        await asyncio.sleep(60) 
        loop = asyncio.get_running_loop()

        achs = await loop.run_in_executor(None, self.get_trophy_achs)
        # for ach in achs:
            # self.unlock_achievement(ach) # TODO - how/when to handle this?


    def update_json_game_time(self, game_id, duration, last_time_played):

        # Get the path of the game times file.
        base_path = os.path.dirname(os.path.realpath(__file__))
        game_times_path = '{}/game_times.json'.format(base_path)
        game_times_json = None

        with open(game_times_path, 'r', encoding='utf-8') as game_times_file:
            game_times_json = json.load(game_times_file)

        old_time_played = game_times_json.get(game_id).get('time_played')
        new_time_played = old_time_played + duration

        game_times_json[game_id]['time_played'] = new_time_played
        game_times_json[game_id]['last_time_played'] = last_time_played

        with open(game_times_path, 'w', encoding='utf-8') as game_times_file:
            json.dump(game_times_json, game_times_file, indent=4)

        self.update_game_time(GameTime(game_id, new_time_played, last_time_played))


    def local_games_list(self):
        local_games = []

        for game in self.games:
            local_games.append(LocalGame(
                game[0],
                LocalGameState.Installed))

        return local_games


    async def get_owned_games(self):
        self.games = self.backend_client.get_games()
        owned_games = []
        
        for game in self.games:
            owned_games.append(Game(
                game[0],
                game[1],
                None,
                LicenseInfo(LicenseType.SinglePurchase, None)))
            
        return owned_games


    async def get_local_games(self):
        return self.local_games_cache
class DolphinPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.NintendoWii, __version__, reader, writer,
                         token)
        self.backend_client = BackendClient()
        self.games = []
        if not os.path.exists(
                os.path.dirname(os.path.realpath(__file__)) +
                r'\gametimes.xml'):
            copyfile(
                os.path.dirname(os.path.realpath(__file__)) +
                r'\files\gametimes.xml',
                os.path.dirname(os.path.realpath(__file__)) +
                r'\gametimes.xml')
        self.game_times = self.get_the_game_times()
        self.local_games_cache = self.local_games_list()
        self.runningGame = self.runningGame = {
            "game_id": "",
            "starting_time": 0,
            "dolphin_running": None,
            "launched": False
        }

    async def authenticate(self, stored_credentials=None):
        return self.do_auth()

    def get_the_game_times(self):
        file = ElementTree.parse(
            os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml')
        game_times = {}
        games_xml = file.getroot()
        for game in games_xml.iter('game'):
            game_id = str(game.find('id').text)
            tt = game.find('time').text
            ltp = game.find('lasttimeplayed').text
            game_times[game_id] = [tt, ltp]
        return game_times

    async def pass_login_credentials(self, step, credentials, cookies):
        return self.do_auth()

    def do_auth(self):
        user_data = {}
        username = user_config.roms_path
        user_data["username"] = username
        self.store_credentials(user_data)
        return Authentication("Dolphin", user_data["username"])

    async def launch_game(self, game_id):
        emu_path = user_config.emu_path
        for game in self.games:
            if game.id == game_id:
                if not user_config.retroarch:
                    openDolphin = Popen([emu_path, "-b", "-e", game.path])
                    gameStartingTime = time.time()
                    self.runningGame = {
                        "game_id": game_id,
                        "starting_time": gameStartingTime,
                        "dolphin_running": openDolphin
                    }
                else:
                    Popen([
                        user_config.retroarch_executable, "-L",
                        user_config.core_path + r'\dolphin_libretro.dll',
                        game.path
                    ])
                break
        return

    async def install_game(self, game_id):
        pass

    async def uninstall_game(self, game_id):
        pass

    async def get_game_time(self, game_id, context=None):
        game_times = self.game_times
        game_time = int(game_times[game_id][0])
        game_time /= 60
        return GameTime(game_id, game_time, game_times[game_id][1])

    def local_games_list(self):
        local_games = []
        for game in self.games:
            local_games.append(LocalGame(game.id, LocalGameState.Installed))
        return local_games

    def tick(self):
        async def update_local_games():
            loop = asyncio.get_running_loop()
            new_local_games_list = await loop.run_in_executor(
                None, self.local_games_list)
            notify_list = self.backend_client.get_state_changes(
                self.local_games_cache, new_local_games_list)
            self.local_games_cache = new_local_games_list
            for local_game_notify in notify_list:
                self.update_local_game_status(local_game_notify)

        file = ElementTree.parse(
            os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml')
        if self.runningGame["dolphin_running"] is not None:
            if self.runningGame["dolphin_running"].poll() is None:
                self.runningGame["launched"] = True
            if self.runningGame["dolphin_running"].poll() is not None:
                if self.runningGame["launched"]:
                    current_time = round(time.time())
                    runtime = time.time() - self.runningGame["starting_time"]
                    games_xml = file.getroot()
                    for game in games_xml.iter('game'):
                        if str(game.find(
                                'id').text) == self.runningGame["game_id"]:
                            previous_time = int(game.find('time').text)
                            total_time = round(previous_time + runtime)
                            game.find('time').text = str(total_time)
                            game.find('lasttimeplayed').text = str(
                                current_time)
                            self.update_game_time(
                                GameTime(self.runningGame["game_id"],
                                         int(total_time / 60), current_time))
                    file.write(
                        os.path.dirname(os.path.realpath(__file__)) +
                        r'\gametimes.xml')
                    self.runningGame["launched"] = False

        asyncio.create_task(update_local_games())

    async def get_owned_games(self):
        self.games = self.backend_client.get_games_db()
        owned_games = []

        for game in self.games:
            owned_games.append(
                Game(game.id, game.name, None,
                     LicenseInfo(LicenseType.SinglePurchase, None)))
        return owned_games

    async def get_local_games(self):
        return self.local_games_cache

    def shutdown(self):
        pass
Ejemplo n.º 12
0
class BNetPlugin(Plugin):
    PRODUCT_DB_PATH = pathlib.Path(AGENT_PATH) / 'product.db'
    CONFIG_PATH = CONFIG_PATH

    def __init__(self, reader, writer, token):
        super().__init__(Platform.Battlenet, version, reader, writer, token)

        log.info(f"Starting Battle.net plugin, version {version}")

        self.bnet_client = None
        self.local_client = LocalClient()
        self.authentication_client = AuthenticatedHttpClient(self)
        self.backend_client = BackendClient(self, self.authentication_client)
        self.social_features = SocialFeatures(self.authentication_client)
        self.error_state = False

        self.running_task = None

        self.database_parser = None
        self.config_parser = None
        self.uninstaller = None

        self.owned_games_cache = []

        self._classic_games_thread = None
        self._battlenet_games_thread = None
        self._installed_battlenet_games = {}
        self._installed_battlenet_games_lock = Lock()

        self.installed_games = self._parse_local_data()
        self.watched_running_games = set()

        self.notifications_enabled = False
        loop = asyncio.get_event_loop()
        loop.create_task(self._register_local_data_watcher())

    async def _register_local_data_watcher(self):
        async def ping(event, interval):
            while True:
                await asyncio.sleep(interval)
                if not self.watched_running_games:
                    if not event.is_set():
                        event.set()

        parse_local_data_event = asyncio.Event()
        FileWatcher(self.CONFIG_PATH, parse_local_data_event, interval=1)
        FileWatcher(self.PRODUCT_DB_PATH, parse_local_data_event, interval=2.5)
        asyncio.create_task(ping(parse_local_data_event, 30))
        while True:
            await parse_local_data_event.wait()
            refreshed_games = self._parse_local_data()
            if not self.notifications_enabled:
                self._update_statuses(refreshed_games, self.installed_games)
            self.installed_games = refreshed_games
            parse_local_data_event.clear()

    async def _notify_about_game_stop(self, game, starting_timeout):
        id_to_watch = game.info.id

        if id_to_watch in self.watched_running_games:
            log.debug(f'Game {id_to_watch} is already watched. Skipping')
            return

        try:
            self.watched_running_games.add(id_to_watch)
            await asyncio.sleep(starting_timeout)
            ProcessProvider().update_games_processes([game])
            log.info(f'Setuping process watcher for {game._processes}')
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(None, game.wait_until_game_stops)
        finally:
            self.update_local_game_status(LocalGame(id_to_watch, LocalGameState.Installed))
            self.watched_running_games.remove(id_to_watch)

    def _update_statuses(self, refreshed_games, previous_games):
        for blizz_id, refr in refreshed_games.items():
            prev = previous_games.get(blizz_id, None)

            if prev is None:
                if refr.playable:
                    log.debug('Detected playable game')
                    state = LocalGameState.Installed
                else:
                    log.debug('Detected installation begin')
                    state = LocalGameState.None_
            elif refr.playable and not prev.playable:
                log.debug('Detected playable game')
                state = LocalGameState.Installed
            elif refr.last_played != prev.last_played:
                log.debug('Detected launched game')
                state = LocalGameState.Installed | LocalGameState.Running
                asyncio.create_task(self._notify_about_game_stop(refr, 5))
            else:
                continue

            log.info(f'Changing game {blizz_id} state to {state}')
            self.update_local_game_status(LocalGame(blizz_id, state))

        for blizz_id, prev in previous_games.items():
            refr = refreshed_games.get(blizz_id, None)
            if refr is None:
                log.debug('Detected uninstalled game')
                state = LocalGameState.None_
                self.update_local_game_status(LocalGame(blizz_id, state))

    def _load_local_files(self):
        try:
            product_db = load_product_db(self.PRODUCT_DB_PATH)
            self.database_parser = DatabaseParser(product_db)
        except FileNotFoundError as e:
            log.warning(f"product.db not found: {repr(e)}")
            return False
        except WindowsError as e:
            # 5 WindowsError access denied
            if e.winerror == 5:
                log.warning(f"product.db not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        except OSError as e:
            if e.errno == errno.EACCES:
                log.warning(f"product.db not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        else:
            if self.local_client.is_installed != self.database_parser.battlenet_present:
                self.local_client.refresh()

        try:
            config = load_config(self.CONFIG_PATH)
            self.config_parser = ConfigParser(config)
        except FileNotFoundError as e:
            log.warning(f"config file not found: {repr(e)}")
            self.config_parser = ConfigParser(None)
            return False
        except WindowsError as e:
            # 5 WindowsError access denied
            if e.winerror == 5:
                log.warning(f"config file not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        except OSError as e:
            if e.errno == errno.EACCES:
                log.warning(f"config file not accessible: {repr(e)}")
                self.config_parser = ConfigParser(None)
                return False
            else:
                raise ()
        return True

    def _get_battlenet_installed_games(self):

        def _add_battlenet_game(config_game, db_game):
            if config_game.uninstall_tag != db_game.uninstall_tag:
                return None
            try:
                blizzard_game = Blizzard[config_game.uid]
            except KeyError:
                log.warning(f'[{config_game.uid}] is not known blizzard game. Skipping')
                return None
            try:
                log.info(f"Adding {blizzard_game.blizzard_id} {blizzard_game.name} to installed games")
                return InstalledGame(
                    blizzard_game,
                    config_game.uninstall_tag,
                    db_game.version,
                    config_game.last_played,
                    db_game.install_path,
                    db_game.playable
                )
            except FileNotFoundError as e:
                log.warning(str(e) + '. Probably outdated product.db after uninstall. Skipping')
            return None

        games = {}
        for db_game in self.database_parser.games:
            for config_game in self.config_parser.games:
                installed_game = _add_battlenet_game(config_game, db_game)
                if installed_game:
                    games[installed_game.info.id] = installed_game
        self._installed_battlenet_games_lock.acquire()
        self._installed_battlenet_games = games
        self._installed_battlenet_games_lock.release()

    def _parse_local_data(self):
        """Game is considered as installed when present in both config and product.db"""
        games = {}
        # give threads 4 seconds to finish
        join_timeout = 4

        if not self._classic_games_thread or not self._classic_games_thread.isAlive():
            self._classic_games_thread = Thread(target=self.local_client.find_classic_games, daemon=True)
            self._classic_games_thread.start()
            log.info("Started classic games thread")

        if not self._load_local_files():
            self._classic_games_thread.join(join_timeout)
            if not self.local_client.classics_lock.acquire(False):
                return []
            else:
                installed_classics = self.local_client.installed_classics
                self.local_client.classics_lock.release()
                return installed_classics

        try:
            if SYSTEM == pf.WINDOWS and self.uninstaller is None:
                uninstaller_path = pathlib.Path(AGENT_PATH) / 'Blizzard Uninstaller.exe'
                self.uninstaller = Uninstaller(uninstaller_path)
        except FileNotFoundError as e:
            log.warning('uninstaller not found' + str(e))

        try:
            if self.local_client.is_installed != self.database_parser.battlenet_present:
                self.local_client.refresh()
            log.info(f"Games found in db {self.database_parser.games}")
            log.info(f"Games found in config {self.config_parser.games}")

            if not self._battlenet_games_thread or not self._battlenet_games_thread.isAlive():
                self._battlenet_games_thread = Thread(target=self._get_battlenet_installed_games, daemon=True)
                self._battlenet_games_thread.start()
                log.info("Started classic games thread")
        except Exception as e:
            log.exception(str(e))
        finally:
            self._classic_games_thread.join(join_timeout)
            self._battlenet_games_thread.join(join_timeout)

            if self.local_client.classics_lock.acquire(False):
                games = self.local_client.installed_classics
                self.local_client.classics_lock.release()

            if self._installed_battlenet_games_lock.acquire(False):
                games = {**self._installed_battlenet_games, **games}
                self._installed_battlenet_games_lock.release()

            return games

    def log_out(self):
        if self.backend_client:
            asyncio.create_task(self.authentication_client.shutdown())
        self.authentication_client.user_details = None
        self.owned_games_cache = []

    async def open_battlenet_browser(self):
        url = f"https://www.blizzard.com/apps/battle.net/desktop"
        log.info(f'Opening battle.net website: {url}')
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(None, lambda x: webbrowser.open(x, autoraise=True), url)

    async def install_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        installed_game = self.installed_games.get(game_id, None)
        if installed_game and os.access(installed_game.install_path, os.F_OK):
            log.warning("Received install command on an already installed game")
            return await self.launch_game(game_id)

        if game_id in Blizzard.legacy_game_ids:
            if SYSTEM == pf.WINDOWS:
                platform = 'windows'
            elif SYSTEM == pf.MACOS:
                platform = 'macos'
            webbrowser.open(f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}")
            return
        try:
            self.local_client.refresh()
            log.info(f'Installing game of id {game_id}')
            self.local_client.install_game(game_id)
        except ClientNotInstalledError as e:
            log.warning(e)
            await self.open_battlenet_browser()
        except Exception as e:
            log.exception(f"Installing game {game_id} failed: {e}")

    def _open_battlenet_at_id(self, game_id):
        try:
            self.local_client.refresh()
            self.local_client.open_battlenet(game_id)
        except Exception as e:
            log.exception(f"Opening battlenet client on specific game_id {game_id} failed {e}")
            try:
                self.local_client.open_battlenet()
            except Exception as e:
                log.exception(f"Opening battlenet client failed {e}")

    async def uninstall_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        if game_id == 'wow_classic':
            # attempting to uninstall classic wow through protocol gives you a message that the game cannot
            # be uninstalled through protocol and you should use battle.net
            return self._open_battlenet_at_id(game_id)

        if SYSTEM == pf.MACOS:
            self._open_battlenet_at_id(game_id)
        else:
            try:
                installed_game = self.installed_games.get(game_id, None)

                if installed_game is None or not os.access(installed_game.install_path, os.F_OK):
                    log.error(f'Cannot uninstall {Blizzard[game_id].uid}')
                    self.update_local_game_status(LocalGame(game_id, LocalGameState.None_))
                    return

                if not isinstance(installed_game.info, ClassicGame):
                    if self.uninstaller is None:
                        raise FileNotFoundError('Uninstaller not found')

                uninstall_tag = installed_game.uninstall_tag
                client_lang = self.config_parser.locale_language
                self.uninstaller.uninstall_game(installed_game, uninstall_tag, client_lang)

            except Exception as e:
                log.exception(f'Uninstalling game {game_id} failed: {e}')

    async def launch_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        try:
            if self.installed_games is None:
                log.error(f'Launching game that is not installed: {game_id}')
                return await self.install_game(game_id)

            game = self.installed_games.get(game_id, None)
            if game is None:
                log.error(f'Launching game that is not installed: {game_id}')
                return await self.install_game(game_id)

            if isinstance(game.info, ClassicGame):
                log.info(f'Launching game of id: {game_id}, {game} at path {os.path.join(game.install_path, game.info.exe)}')
                if SYSTEM == pf.WINDOWS:
                    subprocess.Popen(os.path.join(game.install_path, game.info.exe))
                elif SYSTEM == pf.MACOS:
                    if not game.info.bundle_id:
                        log.warning(f"{game.name} has no bundle id, help by providing us bundle id of this game")
                    subprocess.Popen(['open', '-b', game.info.bundle_id])

                self.update_local_game_status(LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running))
                asyncio.create_task(self._notify_about_game_stop(game, 6))
                return

            self.local_client.refresh()
            log.info(f'Launching game of id: {game_id}, {game}')
            await self.local_client.launch_game(game, wait_sec=60)

            self.update_local_game_status(LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running))
            self.local_client.close_window()
            asyncio.create_task(self._notify_about_game_stop(game, 3))

        except ClientNotInstalledError as e:
            log.warning(e)
            await self.open_battlenet_browser()
        except TimeoutError as e:
            log.warning(str(e))
        except Exception as e:
            log.exception(f"Launching game {game_id} failed: {e}")

    async def authenticate(self, stored_credentials=None):
        try:
            if stored_credentials:
                auth_data = self.authentication_client.process_stored_credentials(stored_credentials)
                try:
                    await self.authentication_client.create_session()
                    await self.backend_client.refresh_cookies()
                    auth_status = await self.backend_client.validate_access_token(auth_data.access_token)
                except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e:
                    raise e
                except Exception:
                    raise InvalidCredentials()
                if self.authentication_client.validate_auth_status(auth_status):
                    self.authentication_client.user_details = await self.backend_client.get_user_info()
                return self.authentication_client.parse_user_details()
            else:
                return self.authentication_client.authenticate_using_login()
        except Exception as e:
            raise e

    async def pass_login_credentials(self, step, credentials, cookies):
        if "logout&app=oauth" in credentials['end_uri']:
            # 2fa expired, repeat authentication
            return self.authentication_client.authenticate_using_login()

        if self.authentication_client.attempted_to_set_battle_tag:
            self.authentication_client.user_details = await self.backend_client.get_user_info()
            return self.authentication_client.parse_auth_after_setting_battletag()

        cookie_jar = self.authentication_client.parse_cookies(cookies)
        auth_data = await self.authentication_client.get_auth_data_login(cookie_jar, credentials)

        try:
            await self.authentication_client.create_session()
            await self.backend_client.refresh_cookies()
        except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e:
            raise e
        except Exception:
            raise InvalidCredentials()

        auth_status = await self.backend_client.validate_access_token(auth_data.access_token)
        if not ("authorities" in auth_status and "IS_AUTHENTICATED_FULLY" in auth_status["authorities"]):
            raise InvalidCredentials()

        self.authentication_client.user_details = await self.backend_client.get_user_info()

        self.authentication_client.set_credentials()

        return self.authentication_client.parse_battletag()

    async def get_friends(self):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()
        friends_list = await self.social_features.get_friends()
        return [FriendInfo(user_id=friend.id.low, user_name='') for friend in friends_list]

    async def get_owned_games(self):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        def _parse_classic_games(classic_games):
            for classic_game in classic_games["classicGames"]:
                log.info(f"looking for {classic_game} in classic games")
                try:
                    blizzard_game = Blizzard[classic_game["localizedGameName"].replace(u'\xa0', ' ')]
                    log.info(f"match! {blizzard_game}")
                    classic_game["titleId"] = blizzard_game.uid
                    classic_game["gameAccountStatus"] = "Good"
                except KeyError:
                    continue

            return classic_games

        def _get_not_added_free_games(owned_games):
            owned_games_ids = []
            for game in owned_games:
                if "titleId" in game:
                    owned_games_ids.append(str(game["titleId"]))

            return [{"titleId": game.blizzard_id,
                    "localizedGameName": game.name,
                    "gameAccountStatus": "Free"}
                    for game in Blizzard.free_games if game.blizzard_id not in owned_games_ids]

        try:
            games = await self.backend_client.get_owned_games()
            classic_games = _parse_classic_games(await self.backend_client.get_owned_classic_games())
            owned_games = games["gameAccounts"] + classic_games["classicGames"]

            # Add wow classic if retail wow is present in owned games
            for owned_game in owned_games.copy():
                if 'titleId' in owned_game:
                    if owned_game['titleId'] == 5730135:
                        owned_games.append({'titleId': 'wow_classic',
                                            'localizedGameName': 'World of Warcraft Classic',
                                            'gameAccountStatus': owned_game['gameAccountStatus']})

            free_games_to_add = _get_not_added_free_games(owned_games)
            owned_games += free_games_to_add
            log.info(f"Owned games {owned_games} with free games")
            self.owned_games_cache = owned_games
            return [
                Game(
                    str(game["titleId"]),
                    game["localizedGameName"],
                    [],
                    LicenseInfo(License_Map[game["gameAccountStatus"]]),
                )
                for game in self.owned_games_cache if "titleId" in game
            ]
        except Exception as e:
            log.exception(f"failed to get owned games: {repr(e)}")
            raise

    async def get_local_games(self):

        try:
            local_games = []
            running_games = ProcessProvider().update_games_processes(self.installed_games.values())
            log.info(f"Installed games {self.installed_games.items()}")
            log.info(f"Running games {running_games}")
            for id_, game in self.installed_games.items():
                if game.playable:
                    state = LocalGameState.Installed
                    if id_ in running_games:
                        state |= LocalGameState.Running
                else:
                    state = LocalGameState.None_
                local_games.append(LocalGame(id_, state))

            return local_games

        except Exception as e:
            log.exception(f"failed to get local games: {str(e)}")
            raise

        finally:
            self.enable_notifications = True

    async def _get_wow_achievements(self):
        achievements = []
        try:
            characters_data = await self.backend_client.get_wow_character_data()
            characters_data = characters_data["characters"]

            wow_character_data = await asyncio.gather(
                *[
                    self.backend_client.get_wow_character_achievements(character["realm"], character["name"])
                    for character in characters_data
                ],
                return_exceptions=True,
            )

            for data in wow_character_data:
                if isinstance(data, requests.Timeout) or isinstance(data, requests.ConnectionError):
                    raise data

            wow_achievement_data = [
                list(
                    zip(
                        data["achievements"]["achievementsCompleted"],
                        data["achievements"]["achievementsCompletedTimestamp"],
                    )
                )
                for data in wow_character_data
                if type(data) is dict
            ]

            already_in = set()

            for char_ach in wow_achievement_data:
                for ach in char_ach:
                    if ach[0] not in already_in:
                        achievements.append(Achievement(achievement_id=ach[0], unlock_time=int(ach[1] / 1000)))
                        already_in.add(ach[0])
        except (AccessTokenExpired, BackendError) as e:
            log.exception(str(e))
        with open('wow.json', 'w') as f:
            f.write(json.dumps(achievements, cls=DataclassJSONEncoder))
        return achievements

    async def _get_sc2_achievements(self):
        account_data = await self.backend_client.get_sc2_player_data(self.authentication_client.user_details["id"])

        # TODO what if more sc2 accounts?
        assert len(account_data) == 1
        account_data = account_data[0]

        profile_data = await self.backend_client.get_sc2_profile_data(
                                                         account_data["regionId"], account_data["realmId"],
                                                         account_data["profileId"]
                                                         )

        sc2_achievement_data = [
            Achievement(achievement_id=achievement["achievementId"], unlock_time=achievement["completionDate"])
            for achievement in profile_data["earnedAchievements"]
            if achievement["isComplete"]
        ]

        with open('sc2.json', 'w') as f:
            f.write(json.dumps(sc2_achievement_data, cls=DataclassJSONEncoder))
        return sc2_achievement_data

    # async def get_unlocked_achievements(self, game_id):
    #     if not self.website_client.is_authenticated():
    #         raise AuthenticationRequired()
    #     try:
    #         if game_id == "21298":
    #             return await self._get_sc2_achievements()
    #         elif game_id == "5730135":
    #             return await self._get_wow_achievements()
    #         else:
    #             return []
    #     except requests.Timeout:
    #         raise BackendTimeout()
    #     except requests.ConnectionError:
    #         raise NetworkError()
    #     except Exception as e:
    #         log.exception(str(e))
    #         return []

    async def _tick_runner(self):
        if not self.bnet_client:
            return
        try:
            self.error_state = await self.bnet_client.tick()
        except Exception as e:
            self.error_state = True
            log.exception(f"error state: {str(e)}")
            raise

    def tick(self):
        if not self.error_state and (not self.running_task or self.running_task.done()):
            self.running_task = asyncio.create_task(self._tick_runner())
        elif self.error_state:
            sys.exit(1)

    def shutdown(self):
        log.info("Plugin shutdown.")
        asyncio.create_task(self.authentication_client.shutdown())
Ejemplo n.º 13
0
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
        self.parsed_local_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()

        if SYSTEM == System.WINDOWS:
            self._parse_local_games()
            self._parse_local_game_ownership()

        await self._parse_club_games()
        try:
            await self._parse_subscription_games()
        except Exception as e:
            log.warning(
                f"Parsing subscriptions failed, most likely account without subscription {repr(e)}"
            )

        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_subscription_games(self):
        subscription_games = []
        sub_response = await self.client.get_subscription()
        if not sub_response:
            return
        for game in sub_response['games']:
            subscription_games.append(
                UbisoftGame(space_id='',
                            launch_id=str(game['uplayGameId']),
                            install_id=str(game['uplayGameId']),
                            third_party_id='',
                            name=game['name'],
                            path='',
                            type=GameType.New,
                            special_registry_path='',
                            exe='',
                            status=GameStatus.Unknown,
                            owned=game['ownership'],
                            activation_id=str(game['id'])))
        self.games_collection.extend(subscription_games)

    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='',
                                            install_id='',
                                            third_party_id='',
                                            name=game['title'],
                                            path='',
                                            type=GameType.New,
                                            special_registry_path='',
                                            exe='',
                                            status=GameStatus.Unknown,
                                            owned=True))
                        else:
                            log.debug(
                                f"Skipped game from Club Request for {game['platform']}: {game['spaceId']}, {game['title']}"
                            )

                self.games_collection.extend(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():
            try:
                configuration_data = self.local_client.read_config()
                p = LocalParser()
                games = []
                for game in p.parse_games(configuration_data):
                    games.append(game)
                self.games_collection.extend(games)
            except scanner.ScannerError as e:
                log.error(
                    f"Scanner error while parsing configuration, yaml is probably corrupted {repr(e)}"
                )

    def _parse_local_game_ownership(self):
        if self.local_client.ownership_accessible():
            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.install_id:
                    if int(game.install_id) in ownership_records:
                        game.owned = True
                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._parse_local_game_ownership()
        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:
            try:
                self.game_status_notifier.update_game(game)
                if game.status != cached_statuses[game.install_id]:
                    log.info(
                        f"Game {game.name} path changed: updating status from {cached_statuses[game.install_id]} to {game.status}"
                    )
                    self.update_local_game_status(game.as_local_game())
                    self.cached_game_statuses[game.install_id] = game.status
            except KeyError:
                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.install_id] = game.status

    if SYSTEM == System.WINDOWS:

        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()
            self.parsed_local_games = True
            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 prepare_game_times_context(self, game_ids):
        return await self.get_playtime(game_ids)

    async def get_game_time(self, game_id, context):
        game_time = context.get(game_id)
        if game_time is None:
            raise UnknownError("Game {} not owned".format(game_id))
        return game_time

    async def get_playtime(self, game_ids):
        if not self.client.is_authenticated():
            raise AuthenticationRequired()

        games_playtime = {}
        blacklist = json.loads(
            self.persistent_cache.get('games_without_stats', '{}'))
        current_time = int(time.time())

        for game_id in game_ids:
            if not self.games_collection.get(game_id):
                await self.get_owned_games()
                break

        for game_id in game_ids:
            try:
                expire_in = blacklist.get(game_id, 0) - current_time
                if expire_in > 0:
                    log.debug(
                        f'Cache: No game stats for {game_id}. Recheck in {expire_in}s'
                    )
                    games_playtime[game_id] = GameTime(game_id, None, None)
                    continue

                game = self.games_collection[game_id]
                if not game.space_id:
                    games_playtime[game_id] = GameTime(game_id, None, None)
                    continue

                try:
                    response = await self.client.get_game_stats(game.space_id)
                except ApplicationError as err:
                    self._game_time_import_failure(game_id, err)
                    continue

                statscards = response.get('Statscards', None)
                if statscards is None:
                    blacklist[
                        game_id] = current_time + 3600 * 24 * 14  # two weeks
                    games_playtime[game_id] = GameTime(game_id, None, None)
                    continue

                playtime, last_played = find_times(statscards, game_id)
                if playtime == 0:
                    playtime = None
                if last_played == 0:
                    last_played = None
                log.info(
                    f'Stats for {game.name}: playtime: {playtime}, last_played: {last_played}'
                )
                games_playtime[game_id] = GameTime(game_id, playtime,
                                                   last_played)

            except Exception as e:
                log.error(
                    f"Getting game times for game {game_id} has crashed: " +
                    repr(e))
                self._game_time_import_failure(game_id, UnknownError())

        self.persistent_cache['games_without_stats'] = json.dumps(blacklist)
        self.push_cache()
        return games_playtime

    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"]
                ]

    if SYSTEM == System.WINDOWS:

        async def launch_game(self, game_id):
            if not self.parsed_local_games:
                await self.get_local_games()
            elif not self.user_can_perform_actions():
                return

            for game in self.games_collection.get_local_games():
                if (game.space_id == game_id or game.install_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:
                        log.debug('Launching game')
                        self.game_status_notifier._legacy_game_launched = True
                        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)
                    self.reset_tick_count()
                    return

            for game in self.games_collection:
                if (game.space_id == game_id
                        or game.install_id == game_id) and game.status in [
                            GameStatus.NotInstalled, GameStatus.Unknown
                        ]:
                    log.warning("Game is not installed, installing")
                    return await self.install_game(game_id)

            log.info("Failed to launch game, launching client instead.")
            self.open_uplay_client()

    async def activate_game(self, activation_id):
        if not await self.client.activate_game(activation_id):
            log.info(f"Couldnt activate game with id {activation_id}")
            return
        log.info(f"Activated game with id {activation_id}")
        timeout = time.time() + 3
        while timeout >= time.time():
            if self.local_client.ownership_changed():
                # Will refresh informations in collection about the game
                await self.get_owned_games()
            await asyncio.sleep(0.1)

    if SYSTEM == System.WINDOWS:

        async def install_game(self, game_id, retry=False):
            log.debug(self.games_collection)
            if not self.user_can_perform_actions():
                return

            for game in self.games_collection:
                game_ids = [game.space_id, game.install_id, game.launch_id]
                if (game_id in game_ids) and game.owned and game.status in [
                        GameStatus.NotInstalled, GameStatus.Unknown
                ]:
                    if game.install_id:
                        log.info(f"Installing game: {game_id}, {game}")
                        subprocess.Popen(
                            f"start uplay://install/{game.install_id}",
                            shell=True)
                        return
                if (game_id
                        in game_ids) and game.status == GameStatus.Installed:
                    log.warning("Game already installed, launching")
                    return await self.launch_game(game_id)

                if (game_id in game_ids
                    ) and not game.owned and game.activation_id and not retry:
                    log.warning("Activating game from subscription")
                    if not self.local_client.is_running():
                        self.open_uplay_client()
                        timeout = time.time() + 10
                        while not self.local_client.is_running(
                        ) and time.time() <= timeout:
                            await asyncio.sleep(0.1)
                    await self.activate_game(game.activation_id)
                    asyncio.create_task(
                        self.install_game(game_id=game_id, retry=True))

            # 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."
            )

    if SYSTEM == System.WINDOWS:

        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("start uplay://", shell=True)

    def open_uplay_browser(self):
        url = '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:
            try:
                if statuses[
                        game.
                        install_id] == GameStatus.Installed and game.status in [
                            GameStatus.NotInstalled, GameStatus.Unknown
                        ]:
                    log.info(
                        f"updating status for {game.name} to installed from not installed"
                    )
                    game.status = GameStatus.Installed
                    self.update_local_game_status(game.as_local_game())
                elif statuses[
                        game.
                        install_id] == GameStatus.Installed and game.status == GameStatus.Running:
                    log.info(
                        f"updating status for {game.name} to installed from running"
                    )
                    game.status = GameStatus.Installed
                    self.update_local_game_status(game.as_local_game())
                    asyncio.create_task(self.prevent_uplay_from_showing())
                elif statuses[
                        game.
                        install_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.install_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())
            except KeyError:
                continue

            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"]
        ]

    async def get_subscriptions(self) -> List[Subscription]:
        sub_status = await self.client.get_subscription()
        sub_status = True if sub_status else False
        return [
            Subscription(
                subscription_name="Uplay+",
                end_time=None,
                owned=sub_status,
                subscription_discovery=SubscriptionDiscovery.AUTOMATIC)
        ]

    async def prepare_subscription_games_context(
            self, subscription_names: List[str]) -> Any:
        sub_games_response = await self.client.get_subscription()
        if sub_games_response:
            return [
                SubscriptionGame(game_title=game['name'],
                                 game_id=str(game['uplayGameId']))
                for game in sub_games_response["games"]
            ]
        return None

    async def get_subscription_games(
            self, subscription_name: str,
            context: Any) -> AsyncGenerator[List[SubscriptionGame], None]:
        yield context

    if SYSTEM == System.WINDOWS:

        async def launch_platform_client(self):
            if self.local_client.is_running():
                log.info(
                    "Launch platform client called but Uplay is already running"
                )
                return
            url = "start uplay://"
            subprocess.Popen(url, shell=True)
            # Uplay tries to get focus a couple of times when being launched
            end_time = time.time() + 15
            while time.time() <= end_time:
                await self.prevent_uplay_from_showing(kill_attempt=False)
                await asyncio.sleep(0.05)

    if SYSTEM == System.WINDOWS:

        async def shutdown_platform_client(self):
            if self.local_client.is_installed:
                subprocess.Popen("taskkill.exe /im \"upc.exe\"", shell=True)

    if SYSTEM == System.WINDOWS:

        async def prevent_uplay_from_showing(self, kill_attempt=True):
            if not self.local_client.is_installed:
                log.info("Local client not installed")
                return
            client_popup_wait_time = 5
            check_frequency_delay = 0.02

            end_time = time.time() + client_popup_wait_time
            hwnd = ctypes.windll.user32.FindWindowW(None, "Uplay")
            while not ctypes.windll.user32.IsWindowVisible(hwnd):
                if time.time() >= end_time:
                    log.info("Timed out post close game uplay popup")
                    break
                hwnd = ctypes.windll.user32.FindWindowW(None, "Uplay")
                await asyncio.sleep(check_frequency_delay)
            if kill_attempt:
                await self.shutdown_platform_client()
            else:
                ctypes.windll.user32.SetForegroundWindow(hwnd)
                ctypes.windll.user32.CloseWindow(hwnd)

    if SYSTEM == System.WINDOWS:

        async def prepare_game_library_settings_context(self, game_ids):
            if self.local_client.settings_accessible():
                library_context = {}
                settings_data = self.local_client.read_settings()
                parser = LocalParser()
                favorite_games, hidden_games = parser.get_game_tags(
                    settings_data)
                for game_id in game_ids:
                    try:
                        game = self.games_collection[game_id]
                    except KeyError:
                        continue
                    library_context[game_id] = {
                        'favorite': game.launch_id in favorite_games,
                        'hidden': game.launch_id in hidden_games
                    }
                return library_context
            return None

        async def get_game_library_settings(self, game_id, context):
            log.debug(f"Context {context}")
            if not context:
                # Unable to retrieve context
                return GameLibrarySettings(game_id, None, None)
            game_library_settings = context.get(game_id)
            if game_library_settings is None:
                # Able to retrieve context but game is not in its values -> It doesnt have any tags or hidden status set
                return GameLibrarySettings(game_id, [], False)
            return GameLibrarySettings(
                game_id,
                ['favorite'] if game_library_settings['favorite'] else [],
                game_library_settings['hidden'])

    def reset_tick_count(self):
        # Resetting tick count ensures that certain operations performed on tick will be made with a known delay.
        self.tick_count = 0

    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

    async def shutdown(self):
        log.info("Plugin shutdown.")
        await self.client.close()
Ejemplo n.º 14
0
class NintendoWiiPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.NintendoWii, __version__, reader, writer, token)
        self.backend_client = BackendClient()
        self.games = []
        self.local_games_cache = self.local_games_list()

        
    async def authenticate(self, stored_credentials=None):
        return self.do_auth()

        
    async def pass_login_credentials(self, step, credentials, cookies):
        return self.do_auth()


    def do_auth(self):
        user_data = {}
        username = user_config.roms_path
        user_data["username"] = username
        self.store_credentials(user_data)
        return Authentication("dolphin_user", user_data["username"])


    async def launch_game(self, game_id):
        emu_path = user_config.emu_path
        
        for game in self.games:
            if str(game[1]) == game_id:
                subprocess.Popen([emu_path, "--exec=" + game[0]])
                break
        return

    async def install_game(self, game_id):
        pass

    async def uninstall_game(self, game_id):
        pass


    def local_games_list(self):
        local_games = []
        for game in self.games:
            local_games.append(
                LocalGame(
                    str(game[1]),
                    LocalGameState.Installed
                )
            )
        return local_games


    def tick(self):

        async def update_local_games():
            loop = asyncio.get_running_loop()
            new_local_games_list = await loop.run_in_executor(None, self.local_games_list)
            notify_list = self.backend_client.get_state_changes(self.local_games_cache, new_local_games_list)
            self.local_games_cache = new_local_games_list
            for local_game_notify in notify_list:
                self.update_local_game_status(local_game_notify)

        asyncio.create_task(update_local_games())


    async def get_owned_games(self):
        self.games = self.backend_client.get_games_gb()
        owned_games = []
        
        for game in self.games:
            owned_games.append(
                Game(
                    str(game[1]),
                    game[2],
                    None,
                    LicenseInfo(LicenseType.SinglePurchase, None)
                )
            )
            
        return owned_games

    async def get_local_games(self):
        return self.local_games_cache
Ejemplo n.º 15
0
class BNetPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Battlenet, version, reader, writer, token)
        self.local_client = LocalClient(self._update_statuses)
        self.authentication_client = AuthenticatedHttpClient(self)
        self.backend_client = BackendClient(self, self.authentication_client)

        self.watched_running_games = set()

    def handshake_complete(self):
        self.create_task(self.__delayed_handshake(), 'delayed handshake')

    async def __delayed_handshake(self):
        """
        Adds some minimal delay on Galaxy start before registering local data watchers.
        Apparently Galaxy may be not ready to receive notifications even after handshake_complete.
        """
        await asyncio.sleep(1)
        self.create_task(self.local_client.register_local_data_watcher(),
                         'local data watcher')
        self.create_task(self.local_client.register_classic_games_updater(),
                         'classic games updater')

    async def _notify_about_game_stop(self, game, starting_timeout):
        id_to_watch = game.info.uid

        if id_to_watch in self.watched_running_games:
            log.debug(f'Game {id_to_watch} is already watched. Skipping')
            return

        try:
            self.watched_running_games.add(id_to_watch)
            await asyncio.sleep(starting_timeout)
            ProcessProvider().update_games_processes([game])
            log.info(f'Setuping process watcher for {game._processes}')
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(None, game.wait_until_game_stops)
        finally:
            self.update_local_game_status(
                LocalGame(id_to_watch, LocalGameState.Installed))
            self.watched_running_games.remove(id_to_watch)

    def _update_statuses(self, refreshed_games, previous_games):
        for blizz_id, refr in refreshed_games.items():
            prev = previous_games.get(blizz_id, None)

            if prev is None:
                if refr.has_galaxy_installed_state:
                    log.debug('Detected playable game')
                    state = LocalGameState.Installed
                else:
                    log.debug('Detected not-fully installed game')
                    continue
            elif refr.has_galaxy_installed_state and not prev.has_galaxy_installed_state:
                log.debug('Detected playable game')
                state = LocalGameState.Installed
            elif refr.last_played != prev.last_played:
                log.debug('Detected launched game')
                state = LocalGameState.Installed | LocalGameState.Running
                self.create_task(self._notify_about_game_stop(refr, 5),
                                 'game stop waiter')
            else:
                continue

            log.info(f'Changing game {blizz_id} state to {state}')
            self.update_local_game_status(LocalGame(blizz_id, state))

        for blizz_id, prev in previous_games.items():
            refr = refreshed_games.get(blizz_id, None)
            if refr is None:
                log.debug('Detected uninstalled game')
                state = LocalGameState.None_
                self.update_local_game_status(LocalGame(blizz_id, state))

    def log_out(self):
        if self.backend_client:
            asyncio.create_task(self.authentication_client.shutdown())
        self.authentication_client.user_details = None

    async def open_battlenet_browser(self):
        url = self.authentication_client.blizzard_battlenet_download_url
        log.info(f'Opening battle.net website: {url}')
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(
            None, lambda x: webbrowser.open(x, autoraise=True), url)

    async def install_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        installed_game = self.local_client.get_installed_games().get(
            game_id, None)
        if installed_game and os.access(installed_game.install_path, os.F_OK):
            log.warning(
                "Received install command on an already installed game")
            return await self.launch_game(game_id)

        if game_id in [classic.uid for classic in Blizzard.CLASSIC_GAMES]:
            if SYSTEM == pf.WINDOWS:
                platform = 'windows'
            elif SYSTEM == pf.MACOS:
                platform = 'macos'
            webbrowser.open(
                f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}"
            )
            return
        try:
            self.local_client.refresh()
            log.info(f'Installing game of id {game_id}')
            self.local_client.install_game(game_id)
        except ClientNotInstalledError as e:
            log.warning(e)
            await self.open_battlenet_browser()
        except Exception as e:
            log.exception(f"Installing game {game_id} failed: {e}")

    def _open_battlenet_at_id(self, game_id):
        try:
            self.local_client.refresh()
            self.local_client.open_battlenet(game_id)
        except Exception as e:
            log.exception(
                f"Opening battlenet client on specific game_id {game_id} failed {e}"
            )
            try:
                self.local_client.open_battlenet()
            except Exception as e:
                log.exception(f"Opening battlenet client failed {e}")

    async def uninstall_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        if game_id == 'wow_classic':
            # attempting to uninstall classic wow through protocol gives you a message that the game cannot
            # be uninstalled through protocol and you should use battle.net
            return self._open_battlenet_at_id(game_id)

        if SYSTEM == pf.MACOS:
            self._open_battlenet_at_id(game_id)
        else:
            try:
                installed_game = self.local_client.get_installed_games().get(
                    game_id, None)

                if installed_game is None or not os.access(
                        installed_game.install_path, os.F_OK):
                    log.error(f'Cannot uninstall {game_id}')
                    self.update_local_game_status(
                        LocalGame(game_id, LocalGameState.None_))
                    return

                if not isinstance(installed_game.info, ClassicGame):
                    if self.local_client.uninstaller is None:
                        raise FileNotFoundError('Uninstaller not found')

                uninstall_tag = installed_game.uninstall_tag
                client_lang = self.local_client.config_parser.locale_language
                self.local_client.uninstaller.uninstall_game(
                    installed_game, uninstall_tag, client_lang)

            except Exception as e:
                log.exception(f'Uninstalling game {game_id} failed: {e}')

    async def launch_game(self, game_id):
        try:
            game = self.local_client.get_installed_games().get(game_id, None)
            if game is None:
                log.error(f'Launching game that is not installed: {game_id}')
                return await self.install_game(game_id)

            if isinstance(game.info, ClassicGame):
                log.info(
                    f'Launching game of id: {game_id}, {game} at path {os.path.join(game.install_path, game.info.exe)}'
                )
                if SYSTEM == pf.WINDOWS:
                    subprocess.Popen(
                        os.path.join(game.install_path, game.info.exe))
                elif SYSTEM == pf.MACOS:
                    if not game.info.bundle_id:
                        log.warning(
                            f"{game.name} has no bundle id, help by providing us bundle id of this game"
                        )
                    subprocess.Popen(['open', '-b', game.info.bundle_id])

                self.update_local_game_status(
                    LocalGame(
                        game_id,
                        LocalGameState.Installed | LocalGameState.Running))
                asyncio.create_task(self._notify_about_game_stop(game, 6))
                return

            self.local_client.refresh()
            log.info(f'Launching game of id: {game_id}, {game}')
            await self.local_client.launch_game(game, wait_sec=60)

            self.update_local_game_status(
                LocalGame(game_id,
                          LocalGameState.Installed | LocalGameState.Running))
            self.local_client.close_window()
            asyncio.create_task(self._notify_about_game_stop(game, 3))

        except ClientNotInstalledError as e:
            log.warning(e)
            await self.open_battlenet_browser()
        except TimeoutError as e:
            log.warning(str(e))
        except Exception as e:
            log.exception(f"Launching game {game_id} failed: {e}")

    async def authenticate(self, stored_credentials=None):
        try:
            if stored_credentials:
                auth_data = self.authentication_client.process_stored_credentials(
                    stored_credentials)
                try:
                    await self.authentication_client.create_session()
                    await self.backend_client.refresh_cookies()
                    auth_status = await self.backend_client.validate_access_token(
                        auth_data.access_token)
                except (BackendNotAvailable, BackendError, NetworkError,
                        UnknownError, BackendTimeout) as e:
                    raise e
                except Exception:
                    raise InvalidCredentials()
                if self.authentication_client.validate_auth_status(
                        auth_status):
                    self.authentication_client.user_details = await self.backend_client.get_user_info(
                    )
                return self.authentication_client.parse_user_details()
            else:
                return self.authentication_client.authenticate_using_login()
        except Exception as e:
            raise e

    async def pass_login_credentials(self, step, credentials, cookies):
        if "logout&app=oauth" in credentials['end_uri']:
            # 2fa expired, repeat authentication
            return self.authentication_client.authenticate_using_login()

        if self.authentication_client.attempted_to_set_battle_tag:
            self.authentication_client.user_details = await self.backend_client.get_user_info(
            )
            return self.authentication_client.parse_auth_after_setting_battletag(
            )

        cookie_jar = self.authentication_client.parse_cookies(cookies)
        auth_data = await self.authentication_client.get_auth_data_login(
            cookie_jar, credentials)

        try:
            await self.authentication_client.create_session()
            await self.backend_client.refresh_cookies()
        except (BackendNotAvailable, BackendError, NetworkError, UnknownError,
                BackendTimeout) as e:
            raise e
        except Exception:
            raise InvalidCredentials()

        auth_status = await self.backend_client.validate_access_token(
            auth_data.access_token)
        if not ("authorities" in auth_status
                and "IS_AUTHENTICATED_FULLY" in auth_status["authorities"]):
            raise InvalidCredentials()

        self.authentication_client.user_details = await self.backend_client.get_user_info(
        )

        self.authentication_client.set_credentials()

        return self.authentication_client.parse_battletag()

    async def get_owned_games(self):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        def _parse_battlenet_games(
                standard_games: dict,
                cn: bool) -> Dict[BlizzardGame, LicenseType]:
            licenses = {
                None: LicenseType.Unknown,
                "Trial": LicenseType.OtherUserLicense,
                "Good": LicenseType.SinglePurchase,
                "Inactive": LicenseType.SinglePurchase,
                "Banned": LicenseType.SinglePurchase,
                "Free": LicenseType.FreeToPlay
            }
            games = {}

            for standard_game in standard_games["gameAccounts"]:
                title_id = standard_game['titleId']
                try:
                    game = Blizzard.game_by_title_id(title_id, cn)
                except KeyError:
                    log.warning(
                        f"Skipping unknown game with titleId: {title_id}")
                else:
                    games[game] = licenses[standard_game.get(
                        "gameAccountStatus")]

            # Add wow classic if retail wow is present in owned games
            wow_license = games.get(Blizzard['wow'])
            if wow_license is not None:
                games[Blizzard['wow_classic']] = wow_license
            return games

        def _parse_classic_games(
                classic_games: dict) -> Dict[ClassicGame, LicenseType]:
            games = {}
            for classic_game in classic_games["classicGames"]:
                sanitized_name = classic_game["localizedGameName"].replace(
                    u'\xa0', ' ')
                for cg in Blizzard.CLASSIC_GAMES:
                    if cg.name == sanitized_name:
                        games[cg] = LicenseType.SinglePurchase
                        break
                else:
                    log.warning(
                        f"Skipping unknown classic game with name: {sanitized_name}"
                    )
            return games

        cn = self.authentication_client.region == 'cn'

        battlenet_games = _parse_battlenet_games(
            await self.backend_client.get_owned_games(), cn)
        classic_games = _parse_classic_games(
            await self.backend_client.get_owned_classic_games())
        owned_games: Dict[BlizzardGame, LicenseType] = {
            **battlenet_games,
            **classic_games
        }

        for game in Blizzard.try_for_free_games(cn):
            if game not in owned_games:
                owned_games[game] = LicenseType.FreeToPlay

        return [
            Game(game.uid, game.name, None, LicenseInfo(license_type))
            for game, license_type in owned_games.items()
        ]

    async def get_local_games(self):
        timeout = time.time() + 2

        try:
            translated_installed_games = []

            while not self.local_client.games_finished_parsing():
                await asyncio.sleep(0.1)
                if time.time() >= timeout:
                    break

            running_games = self.local_client.get_running_games()
            installed_games = self.local_client.get_installed_games()
            log.info(f"Installed games {installed_games.items()}")
            log.info(f"Running games {running_games}")
            for uid, game in installed_games.items():
                if game.has_galaxy_installed_state:
                    state = LocalGameState.Installed
                    if uid in running_games:
                        state |= LocalGameState.Running
                    translated_installed_games.append(LocalGame(uid, state))
            self.local_client.installed_games_cache = installed_games
            return translated_installed_games

        except Exception as e:
            log.exception(f"failed to get local games: {str(e)}")
            raise

    async def get_game_time(self, game_id, context):
        total_time = None
        last_played_time = None

        blizzard_game = Blizzard[game_id]

        if blizzard_game.name == "Overwatch":
            total_time = await self._get_overwatch_time()
            log.debug(f"Gametime for Overwatch is {total_time} minutes.")

        for config_info in self.local_client.config_parser.games:
            if config_info.uid == blizzard_game.uid:
                if config_info.last_played is not None:
                    last_played_time = int(config_info.last_played)
                break

        return GameTime(game_id, total_time, last_played_time)

    async def _get_overwatch_time(self) -> Union[None, int]:
        log.debug("Fetching playtime for Overwatch...")
        player_data = await self.backend_client.get_ow_player_data()
        if 'message' in player_data:  # user not found
            log.error('No Overwatch profile found.')
            return None
        if player_data['private'] == True:
            log.info('Unable to get data as Overwatch profile is private.')
            return None
        qp_time = player_data['playtime'].get('quickplay')
        if qp_time is None:  # user has not played quick play
            return 0
        if qp_time.count(':') == 1:  # minutes and seconds
            match = re.search('(?:(?P<m>\\d+):)(?P<s>\\d+)', qp_time)
            if match:
                return int(match.group('m'))
        elif qp_time.count(':') == 2:  # hours, minutes and seconds
            match = re.search('(?:(?P<h>\\d+):)(?P<m>\\d+)', qp_time)
            if match:
                return int(match.group('h')) * 60 + int(match.group('m'))
        raise UnknownBackendResponse(
            f'Unknown Overwatch API playtime format: {qp_time}')

    async def _get_wow_achievements(self):
        achievements = []
        try:
            characters_data = await self.backend_client.get_wow_character_data(
            )
            characters_data = characters_data["characters"]

            wow_character_data = await asyncio.gather(
                *[
                    self.backend_client.get_wow_character_achievements(
                        character["realm"], character["name"])
                    for character in characters_data
                ],
                return_exceptions=True,
            )

            for data in wow_character_data:
                if isinstance(data, requests.Timeout) or isinstance(
                        data, requests.ConnectionError):
                    raise data

            wow_achievement_data = [
                list(
                    zip(
                        data["achievements"]["achievementsCompleted"],
                        data["achievements"]["achievementsCompletedTimestamp"],
                    )) for data in wow_character_data if type(data) is dict
            ]

            already_in = set()

            for char_ach in wow_achievement_data:
                for ach in char_ach:
                    if ach[0] not in already_in:
                        achievements.append(
                            Achievement(achievement_id=ach[0],
                                        unlock_time=int(ach[1] / 1000)))
                        already_in.add(ach[0])
        except (AccessTokenExpired, BackendError) as e:
            log.exception(str(e))
        with open('wow.json', 'w') as f:
            f.write(json.dumps(achievements, cls=DataclassJSONEncoder))
        return achievements

    async def _get_sc2_achievements(self):
        account_data = await self.backend_client.get_sc2_player_data(
            self.authentication_client.user_details["id"])

        # TODO what if more sc2 accounts?
        assert len(account_data) == 1
        account_data = account_data[0]

        profile_data = await self.backend_client.get_sc2_profile_data(
            account_data["regionId"], account_data["realmId"],
            account_data["profileId"])

        sc2_achievement_data = [
            Achievement(achievement_id=achievement["achievementId"],
                        unlock_time=achievement["completionDate"])
            for achievement in profile_data["earnedAchievements"]
            if achievement["isComplete"]
        ]

        with open('sc2.json', 'w') as f:
            f.write(json.dumps(sc2_achievement_data, cls=DataclassJSONEncoder))
        return sc2_achievement_data

    # async def get_unlocked_achievements(self, game_id):
    #     if not self.website_client.is_authenticated():
    #         raise AuthenticationRequired()
    #     try:
    #         if game_id == "21298":
    #             return await self._get_sc2_achievements()
    #         elif game_id == "5730135":
    #             return await self._get_wow_achievements()
    #         else:
    #             return []
    #     except requests.Timeout:
    #         raise BackendTimeout()
    #     except requests.ConnectionError:
    #         raise NetworkError()
    #     except Exception as e:
    #         log.exception(str(e))
    #         return []

    async def launch_platform_client(self):
        if self.local_client.is_running():
            log.info(
                "Launch platform client called but client is already running")
            return
        self.local_client.open_battlenet()
        await self.local_client.prevent_battlenet_from_showing()

    async def shutdown_platform_client(self):
        await self.local_client.shutdown_platform_client()

    async def shutdown(self):
        log.info("Plugin shutdown.")
        await self.authentication_client.shutdown()
Ejemplo n.º 16
0
class DolphinPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.NintendoGameCube, __version__, reader,
                         writer, token)
        self.backend_client = BackendClient()
        self.games = []
        self.game_times = get_the_game_times()
        self.local_games_cache = self.local_games_list()

    async def authenticate(self, stored_credentials=None):
        return self.do_auth()

    async def pass_login_credentials(self, step, credentials, cookies):
        return self.do_auth()

    def do_auth(self):
        user_data = {}
        username = user_config.roms_path
        user_data["username"] = username
        self.store_credentials(user_data)
        return Authentication("Dolphin", user_data["username"])

    async def launch_game(self, game_id):
        emu_path = user_config.emu_path
        for game in self.games:
            if str(game[1]) == game_id:
                if user_config.retroarch is not True:
                    subprocess.Popen([emu_path, "-b", "-e", game[0]])
                    subprocess.Popen([
                        os.path.dirname(os.path.realpath(__file__)) +
                        r'\TimeTracker.exe', game_id, game_id
                    ])
                else:
                    subprocess.Popen([
                        user_config.retroarch_executable, "-L",
                        user_config.core_path + r'\dolphin_libretro.dll',
                        game[0]
                    ])
                break
        return

    async def install_game(self, game_id):
        pass

    async def uninstall_game(self, game_id):
        pass

    async def get_game_time(self, game_id, context=None):
        self.game_times = get_the_game_times()
        game_times = self.game_times
        game_time = int(game_times[game_id][0])
        game_time /= 60
        return GameTime(game_id, game_time, game_times[game_id][1])

    def local_games_list(self):
        local_games = []
        for game in self.games:
            local_games.append(
                LocalGame(str(game[1]), LocalGameState.Installed))
        return local_games

    def tick(self):
        async def update_local_games():
            loop = asyncio.get_running_loop()
            new_local_games_list = await loop.run_in_executor(
                None, self.local_games_list)
            notify_list = self.backend_client.get_state_changes(
                self.local_games_cache, new_local_games_list)
            self.local_games_cache = new_local_games_list
            for local_game_notify in notify_list:
                self.update_local_game_status(local_game_notify)

        asyncio.create_task(update_local_games())

    async def get_owned_games(self):
        self.games = self.backend_client.get_games_db()
        owned_games = []

        for game in self.games:
            owned_games.append(
                Game(str(game[1]), game[2], None,
                     LicenseInfo(LicenseType.SinglePurchase, None)))

        return owned_games

    async def get_local_games(self):
        return self.local_games_cache

    def shutdown(self):
        pass
Ejemplo n.º 17
0
 def __init__(self, reader, writer, token):
     super().__init__(Platform.PlayStation2, __version__, reader, writer, token)
     self.backend_client = BackendClient()
     self.games = []
     self.local_games_cache = self.local_games_list()
Ejemplo n.º 18
0
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())
Ejemplo n.º 19
0
class BNetPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Battlenet, version, reader, writer, token)
        self.local_client = LocalClient(self._update_statuses)
        self.authentication_client = AuthenticatedHttpClient(self)
        self.backend_client = BackendClient(self, self.authentication_client)

        self.owned_games_cache = []
        self.watched_running_games = set()
        self.local_games_called = False

    async def _notify_about_game_stop(self, game, starting_timeout):
        if not self.local_games_called:
            return
        id_to_watch = game.info.id

        if id_to_watch in self.watched_running_games:
            log.debug(f'Game {id_to_watch} is already watched. Skipping')
            return

        try:
            self.watched_running_games.add(id_to_watch)
            await asyncio.sleep(starting_timeout)
            ProcessProvider().update_games_processes([game])
            log.info(f'Setuping process watcher for {game._processes}')
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(None, game.wait_until_game_stops)
        finally:
            self.update_local_game_status(
                LocalGame(id_to_watch, LocalGameState.Installed))
            self.watched_running_games.remove(id_to_watch)

    def _update_statuses(self, refreshed_games, previous_games):
        if not self.local_games_called:
            return
        for blizz_id, refr in refreshed_games.items():
            prev = previous_games.get(blizz_id, None)

            if prev is None:
                if refr.playable:
                    log.debug('Detected playable game')
                    state = LocalGameState.Installed
                else:
                    log.debug('Detected installation begin')
                    state = LocalGameState.None_
            elif refr.playable and not prev.playable:
                log.debug('Detected playable game')
                state = LocalGameState.Installed
            elif refr.last_played != prev.last_played:
                log.debug('Detected launched game')
                state = LocalGameState.Installed | LocalGameState.Running
                asyncio.create_task(self._notify_about_game_stop(refr, 5))
            else:
                continue

            log.info(f'Changing game {blizz_id} state to {state}')
            self.update_local_game_status(LocalGame(blizz_id, state))

        for blizz_id, prev in previous_games.items():
            refr = refreshed_games.get(blizz_id, None)
            if refr is None:
                log.debug('Detected uninstalled game')
                state = LocalGameState.None_
                self.update_local_game_status(LocalGame(blizz_id, state))

    def log_out(self):
        if self.backend_client:
            asyncio.create_task(self.authentication_client.shutdown())
        self.authentication_client.user_details = None
        self.owned_games_cache = []

    async def open_battlenet_browser(self):
        url = self.authentication_client.blizzard_battlenet_download_url
        log.info(f'Opening battle.net website: {url}')
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(
            None, lambda x: webbrowser.open(x, autoraise=True), url)

    async def install_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        installed_game = self.local_client.get_installed_games().get(
            game_id, None)
        if installed_game and os.access(installed_game.install_path, os.F_OK):
            log.warning(
                "Received install command on an already installed game")
            return await self.launch_game(game_id)

        if game_id in Blizzard.legacy_game_ids:
            if SYSTEM == pf.WINDOWS:
                platform = 'windows'
            elif SYSTEM == pf.MACOS:
                platform = 'macos'
            webbrowser.open(
                f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}"
            )
            return
        try:
            self.local_client.refresh()
            log.info(f'Installing game of id {game_id}')
            self.local_client.install_game(game_id)
        except ClientNotInstalledError as e:
            log.warning(e)
            await self.open_battlenet_browser()
        except Exception as e:
            log.exception(f"Installing game {game_id} failed: {e}")

    def _open_battlenet_at_id(self, game_id):
        try:
            self.local_client.refresh()
            self.local_client.open_battlenet(game_id)
        except Exception as e:
            log.exception(
                f"Opening battlenet client on specific game_id {game_id} failed {e}"
            )
            try:
                self.local_client.open_battlenet()
            except Exception as e:
                log.exception(f"Opening battlenet client failed {e}")

    async def uninstall_game(self, game_id):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        if game_id == 'wow_classic':
            # attempting to uninstall classic wow through protocol gives you a message that the game cannot
            # be uninstalled through protocol and you should use battle.net
            return self._open_battlenet_at_id(game_id)

        if SYSTEM == pf.MACOS:
            self._open_battlenet_at_id(game_id)
        else:
            try:
                installed_game = self.local_client.get_installed_games().get(
                    game_id, None)

                if installed_game is None or not os.access(
                        installed_game.install_path, os.F_OK):
                    log.error(f'Cannot uninstall {Blizzard[game_id].uid}')
                    self.update_local_game_status(
                        LocalGame(game_id, LocalGameState.None_))
                    return

                if not isinstance(installed_game.info, ClassicGame):
                    if self.local_client.uninstaller is None:
                        raise FileNotFoundError('Uninstaller not found')

                uninstall_tag = installed_game.uninstall_tag
                client_lang = self.local_client.config_parser.locale_language
                self.local_client.uninstaller.uninstall_game(
                    installed_game, uninstall_tag, client_lang)

            except Exception as e:
                log.exception(f'Uninstalling game {game_id} failed: {e}')

    async def launch_game(self, game_id):
        if not self.local_games_called:
            await self.get_local_games()

        try:
            if self.local_client.get_installed_games() is None:
                log.error(f'Launching game that is not installed: {game_id}')
                return await self.install_game(game_id)

            game = self.local_client.get_installed_games().get(game_id, None)
            if game is None:
                log.error(f'Launching game that is not installed: {game_id}')
                return await self.install_game(game_id)

            if isinstance(game.info, ClassicGame):
                log.info(
                    f'Launching game of id: {game_id}, {game} at path {os.path.join(game.install_path, game.info.exe)}'
                )
                if SYSTEM == pf.WINDOWS:
                    subprocess.Popen(
                        os.path.join(game.install_path, game.info.exe))
                elif SYSTEM == pf.MACOS:
                    if not game.info.bundle_id:
                        log.warning(
                            f"{game.name} has no bundle id, help by providing us bundle id of this game"
                        )
                    subprocess.Popen(['open', '-b', game.info.bundle_id])

                self.update_local_game_status(
                    LocalGame(
                        game_id,
                        LocalGameState.Installed | LocalGameState.Running))
                asyncio.create_task(self._notify_about_game_stop(game, 6))
                return

            self.local_client.refresh()
            log.info(f'Launching game of id: {game_id}, {game}')
            await self.local_client.launch_game(game, wait_sec=60)

            self.update_local_game_status(
                LocalGame(game_id,
                          LocalGameState.Installed | LocalGameState.Running))
            self.local_client.close_window()
            asyncio.create_task(self._notify_about_game_stop(game, 3))

        except ClientNotInstalledError as e:
            log.warning(e)
            await self.open_battlenet_browser()
        except TimeoutError as e:
            log.warning(str(e))
        except Exception as e:
            log.exception(f"Launching game {game_id} failed: {e}")

    async def authenticate(self, stored_credentials=None):
        try:
            if stored_credentials:
                auth_data = self.authentication_client.process_stored_credentials(
                    stored_credentials)
                try:
                    await self.authentication_client.create_session()
                    await self.backend_client.refresh_cookies()
                    auth_status = await self.backend_client.validate_access_token(
                        auth_data.access_token)
                except (BackendNotAvailable, BackendError, NetworkError,
                        UnknownError, BackendTimeout) as e:
                    raise e
                except Exception:
                    raise InvalidCredentials()
                if self.authentication_client.validate_auth_status(
                        auth_status):
                    self.authentication_client.user_details = await self.backend_client.get_user_info(
                    )
                return self.authentication_client.parse_user_details()
            else:
                return self.authentication_client.authenticate_using_login()
        except Exception as e:
            raise e

    async def pass_login_credentials(self, step, credentials, cookies):
        if "logout&app=oauth" in credentials['end_uri']:
            # 2fa expired, repeat authentication
            return self.authentication_client.authenticate_using_login()

        if self.authentication_client.attempted_to_set_battle_tag:
            self.authentication_client.user_details = await self.backend_client.get_user_info(
            )
            return self.authentication_client.parse_auth_after_setting_battletag(
            )

        cookie_jar = self.authentication_client.parse_cookies(cookies)
        auth_data = await self.authentication_client.get_auth_data_login(
            cookie_jar, credentials)

        try:
            await self.authentication_client.create_session()
            await self.backend_client.refresh_cookies()
        except (BackendNotAvailable, BackendError, NetworkError, UnknownError,
                BackendTimeout) as e:
            raise e
        except Exception:
            raise InvalidCredentials()

        auth_status = await self.backend_client.validate_access_token(
            auth_data.access_token)
        if not ("authorities" in auth_status
                and "IS_AUTHENTICATED_FULLY" in auth_status["authorities"]):
            raise InvalidCredentials()

        self.authentication_client.user_details = await self.backend_client.get_user_info(
        )

        self.authentication_client.set_credentials()

        return self.authentication_client.parse_battletag()

    async def get_owned_games(self):
        if not self.authentication_client.is_authenticated():
            raise AuthenticationRequired()

        def _parse_classic_games(classic_games):
            for classic_game in classic_games["classicGames"]:
                log.info(f"looking for {classic_game} in classic games")
                try:
                    blizzard_game = Blizzard[
                        classic_game["localizedGameName"].replace(
                            u'\xa0', ' ')]
                    log.info(f"match! {blizzard_game}")
                    classic_game["titleId"] = blizzard_game.uid
                    classic_game["gameAccountStatus"] = "Good"
                except KeyError:
                    continue

            return classic_games

        def _get_not_added_free_games(owned_games):
            owned_games_ids = []
            for game in owned_games:
                if "titleId" in game:
                    owned_games_ids.append(str(game["titleId"]))

            return [{
                "titleId": game.blizzard_id,
                "localizedGameName": game.name,
                "gameAccountStatus": "Free"
            } for game in Blizzard.free_games
                    if game.blizzard_id not in owned_games_ids]

        try:
            games = await self.backend_client.get_owned_games()
            classic_games = _parse_classic_games(
                await self.backend_client.get_owned_classic_games())
            owned_games = games["gameAccounts"] + classic_games["classicGames"]

            # Add wow classic if retail wow is present in owned games
            for owned_game in owned_games.copy():
                if 'titleId' in owned_game:
                    if owned_game['titleId'] == 5730135:
                        owned_games.append({
                            'titleId':
                            'wow_classic',
                            'localizedGameName':
                            'World of Warcraft Classic',
                            'gameAccountStatus':
                            owned_game['gameAccountStatus']
                        })

            free_games_to_add = _get_not_added_free_games(owned_games)
            owned_games += free_games_to_add
            self.owned_games_cache = owned_games
            return [
                Game(
                    str(game["titleId"]),
                    game["localizedGameName"],
                    [],
                    LicenseInfo(License_Map[game["gameAccountStatus"]]),
                ) for game in self.owned_games_cache if "titleId" in game
            ]
        except Exception as e:
            log.exception(f"failed to get owned games: {repr(e)}")
            raise

    async def get_local_games(self):
        timeout = time.time() + 2

        try:
            translated_installed_games = []

            while not self.local_client.games_finished_parsing():
                await asyncio.sleep(0.1)
                if time.time() >= timeout:
                    break

            running_games = self.local_client.get_running_games()
            installed_games = self.local_client.get_installed_games()
            log.info(f"Installed games {installed_games.items()}")
            log.info(f"Running games {running_games}")
            for id_, game in installed_games.items():
                if game.playable:
                    state = LocalGameState.Installed
                    if id_ in running_games:
                        state |= LocalGameState.Running
                else:
                    state = LocalGameState.None_
                translated_installed_games.append(LocalGame(id_, state))
            self.local_client.installed_games_cache = installed_games
            return translated_installed_games

        except Exception as e:
            log.exception(f"failed to get local games: {str(e)}")
            raise

        finally:
            self.local_games_called = True

    async def get_game_time(self, game_id, context):
        game_time_minutes = None
        if game_id == "5272175":
            game_time_minutes = await self._get_overwatch_time()
            log.debug(
                f"Gametime for Overwatch is {game_time_minutes} minutes.")
        return GameTime(game_id, game_time_minutes, None)

    async def _get_overwatch_time(self) -> Union[None, int]:
        log.debug("Fetching playtime for Overwatch...")
        player_data = await self.backend_client.get_ow_player_data()
        if 'message' in player_data:  # user not found... unfortunately no 404 status code is returned :/
            log.error('No Overwatch profile found.')
            return None
        if player_data['private'] == True:
            log.info('Unable to get data as Overwatch profile is private.')
            return None
        qp_time = player_data['playtime']['quickplay']
        if qp_time is None:  # user has not played quick play
            return 0
        if qp_time.count(':') == 1:  # minutes and seconds
            match = re.search('(?:(?P<m>\\d+):)(?P<s>\\d+)', qp_time)
            if match:
                return int(match.group('m'))
        elif qp_time.count(':') == 2:  # hours, minutes and seconds
            match = re.search('(?:(?P<h>\\d+):)(?P<m>\\d+)', qp_time)
            if match:
                return int(match.group('h')) * 60 + int(match.group('m'))
        raise UnknownBackendResponse(
            f'Unknown Overwatch API playtime format: {qp_time}')

    async def _get_wow_achievements(self):
        achievements = []
        try:
            characters_data = await self.backend_client.get_wow_character_data(
            )
            characters_data = characters_data["characters"]

            wow_character_data = await asyncio.gather(
                *[
                    self.backend_client.get_wow_character_achievements(
                        character["realm"], character["name"])
                    for character in characters_data
                ],
                return_exceptions=True,
            )

            for data in wow_character_data:
                if isinstance(data, requests.Timeout) or isinstance(
                        data, requests.ConnectionError):
                    raise data

            wow_achievement_data = [
                list(
                    zip(
                        data["achievements"]["achievementsCompleted"],
                        data["achievements"]["achievementsCompletedTimestamp"],
                    )) for data in wow_character_data if type(data) is dict
            ]

            already_in = set()

            for char_ach in wow_achievement_data:
                for ach in char_ach:
                    if ach[0] not in already_in:
                        achievements.append(
                            Achievement(achievement_id=ach[0],
                                        unlock_time=int(ach[1] / 1000)))
                        already_in.add(ach[0])
        except (AccessTokenExpired, BackendError) as e:
            log.exception(str(e))
        with open('wow.json', 'w') as f:
            f.write(json.dumps(achievements, cls=DataclassJSONEncoder))
        return achievements

    async def _get_sc2_achievements(self):
        account_data = await self.backend_client.get_sc2_player_data(
            self.authentication_client.user_details["id"])

        # TODO what if more sc2 accounts?
        assert len(account_data) == 1
        account_data = account_data[0]

        profile_data = await self.backend_client.get_sc2_profile_data(
            account_data["regionId"], account_data["realmId"],
            account_data["profileId"])

        sc2_achievement_data = [
            Achievement(achievement_id=achievement["achievementId"],
                        unlock_time=achievement["completionDate"])
            for achievement in profile_data["earnedAchievements"]
            if achievement["isComplete"]
        ]

        with open('sc2.json', 'w') as f:
            f.write(json.dumps(sc2_achievement_data, cls=DataclassJSONEncoder))
        return sc2_achievement_data

    # async def get_unlocked_achievements(self, game_id):
    #     if not self.website_client.is_authenticated():
    #         raise AuthenticationRequired()
    #     try:
    #         if game_id == "21298":
    #             return await self._get_sc2_achievements()
    #         elif game_id == "5730135":
    #             return await self._get_wow_achievements()
    #         else:
    #             return []
    #     except requests.Timeout:
    #         raise BackendTimeout()
    #     except requests.ConnectionError:
    #         raise NetworkError()
    #     except Exception as e:
    #         log.exception(str(e))
    #         return []

    async def launch_platform_client(self):
        if self.local_client.is_running():
            log.info(
                "Launch platform client called but client is already running")
            return
        self.local_client.open_battlenet()
        await self.local_client.prevent_battlenet_from_showing()

    async def shutdown_platform_client(self):
        await self.local_client.shutdown_platform_client()

    async def shutdown(self):
        log.info("Plugin shutdown.")
        await self.authentication_client.shutdown()
Ejemplo n.º 20
0
class SuperNintendoEntertainmentSystemPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.SuperNintendoEntertainmentSystem, __version__, reader, writer, token)
        self.backend_client = BackendClient(self)
        self.games = []
        self.local_games_cache = []
        self.proc = None
        self.running_game_id = ""
        self.tick_count = 0

        self.create_task(self._update_local_games(), "Update local games")

        
    async def authenticate(self, stored_credentials=None):
        return self._do_auth()

        
    async def pass_login_credentials(self, step, credentials, cookies):
        return self._do_auth()


    def _do_auth(self) -> Authentication:
        user_data = {}
        username = user_config.roms_path
        user_data["username"] = username
        self.store_credentials(user_data)
        return Authentication("bsnes_user", user_data["username"])


    async def launch_game(self, game_id):
        self.running_game_id = game_id
        emu_path = user_config.emu_path
        fullscreen = user_config.emu_fullscreen

        self._launch_game(game_id, emu_path, fullscreen)
        self.backend_client._set_session_start()


    def _launch_game(self, game_id, emu_path, fullscreen) -> None:
        ''' Returns None

        Interprets user configurated options and launches Bsnes with the chosen rom
        '''
        for game in self.games:
            if game.id == game_id:
                args = [emu_path]
                if fullscreen:
                    args.append("--fullscreen")
                args.append(game.path)
                self.proc = subprocess.Popen(args)
                break


    # Only as placeholders so the launch game feature is recognized
    async def install_game(self, game_id):
        pass

    async def uninstall_game(self, game_id):
        pass


    async def prepare_game_times_context(self, game_ids):
        return self._get_games_times_dict()

    
    async def get_game_time(self, game_id, context):
        game_time = context.get(game_id)
        return game_time


    def _get_games_times_dict(self) -> dict:
        ''' Returns a dict of GameTime objects
        
        Creates and reads the game_times.json file
        '''
        base_dir = os.path.dirname(os.path.realpath(__file__))
        data = {}
        game_times = {}
        path = "{}/game_times.json".format(base_dir)
        
        # Check if the file exists, otherwise create it with defaults
        if not os.path.exists(path):
            for game in self.games:
                data[game.id] = {
                    "name" : game.name,
                    "time_played" : 0,
                    "last_time_played" : None
                }

            with open(path, "w", encoding="utf-8") as game_times_file:
                json.dump(data, game_times_file, indent=4)
        
        # Now read it and return the game times
        with open(path, encoding="utf-8") as game_times_file:
            parsed_game_times_file = json.load(game_times_file)

        for entry in parsed_game_times_file:
            game_id = entry
            time_played = parsed_game_times_file.get(entry).get("time_played")
            last_time_played = parsed_game_times_file.get(entry).get("last_time_played")
            game_times[game_id] = GameTime(game_id, time_played, last_time_played)

        return game_times


    def _local_games_list(self) -> list:
        ''' Returns a list of LocalGame objects

        Goes through retrieved games and adds them as local games with default state of "Installed"
        '''
        local_games = []        
        for game in self.games:
            local_games.append(
                LocalGame(
                    game.id,
                    LocalGameState.Installed
                )
            )
        return local_games


    def tick(self):
        self.tick_count += 1
        
        self._check_proc_status()
        self.create_task(self._update_local_games(), "Update local games")
        if self.tick_count % 3 == 0:
            self.create_task(self._update_all_game_times(), "Update all game times")


    def _check_proc_status(self) -> None:
        try:
            if(self.proc.poll() is not None):
                self.backend_client._set_session_end()
                session_duration = self.backend_client._get_session_duration()
                last_time_played = int(time.time())
                self._update_game_time(self.running_game_id, session_duration, last_time_played)
                self.proc = None
        except AttributeError:
            pass


    async def _update_local_games(self) -> None:
        loop = asyncio.get_running_loop()
        new_list = await loop.run_in_executor(None, self._local_games_list)
        notify_list = self.backend_client._get_state_changes(self.local_games_cache, new_list)
        self.local_games_cache = new_list
        for local_game_notify in notify_list:
            self.update_local_game_status(local_game_notify)


    async def _update_all_game_times(self) -> None:
        await asyncio.sleep(60) # Leave time for Galaxy to fetch games before updating times
        loop = asyncio.get_running_loop()
        new_game_times = await loop.run_in_executor(None, self._get_games_times_dict)
        for game_time in new_game_times:
            self.update_game_time(new_game_times[game_time])


    def _update_game_time(self, game_id, session_duration, last_time_played) -> None:
        ''' Returns None 
        
        Update the game time of a single game
        '''
        base_dir = os.path.dirname(os.path.realpath(__file__))
        game_times_path = "{}/game_times.json".format(base_dir)

        with open(game_times_path, encoding="utf-8") as game_times_file:
            data = json.load(game_times_file)

        data[game_id]["time_played"] = data.get(game_id).get("time_played") + session_duration
        data[game_id]["last_time_played"] = last_time_played

        with open(game_times_path, "w", encoding="utf-8") as game_times_file:
            json.dump(data, game_times_file, indent=4)

        self.update_game_time(GameTime(game_id, data.get(game_id).get("time_played"), last_time_played))


    async def get_owned_games(self):
        owned_games = []
        self.games = self.backend_client._get_games_giant_bomb()
        
        for game in self.games:
            owned_games.append(
                Game(
                    game.id,
                    game.name,
                    None,
                    LicenseInfo(LicenseType.SinglePurchase, None)
                )
            )   
        return owned_games
        

    async def get_local_games(self):
        return self.local_games_cache