[cli] add CLI interface for achievements

This commit is contained in:
Stelios Tsampas 2025-12-20 01:50:00 +02:00
parent d976c9e6a5
commit 592e30cafc
2 changed files with 127 additions and 10 deletions

View file

@ -66,9 +66,9 @@ class LegendaryCLI:
@staticmethod
def _print_json(data, pretty=False):
if pretty:
print(json.dumps(data, indent=2, sort_keys=True))
print(json.dumps(data, indent=2, sort_keys=True, default=str))
else:
print(json.dumps(data))
print(json.dumps(data, default=str))
def auth(self, args):
if args.auth_delete:
@ -2626,6 +2626,57 @@ class LegendaryCLI:
self.core.install_game(igame)
logger.info('Finished.')
def achievements(self, args):
if not self.core.login():
logger.error('Login failed! Unable to check for EULAs.')
exit(1)
app_name = self._resolve_aliases(args.app_name)
game = self.core.get_game(app_name, update_meta=True)
if not game:
logger.error(f'No game found for "{app_name}"')
return
achievements = self.core.get_achievements(game, update=True)
if not achievements:
logger.info(f'No achievements found for "{game.app_name}"')
return
if args.json:
self._print_json(achievements, args.pretty_json)
return
print(f'* Achievements for "{game.app_title}"')
print(f' Total achievements: {achievements["total_achievements"]}')
print(f' Completed achievements: {achievements["user_unlocked"]}')
print(f' Total XP: {achievements["total_product_xp"]}')
print(f' Player XP: {achievements["user_xp"]}')
print(f' Player awards: {achievements["user_awards"]}')
completed = filter(lambda x: x['unlock_date'] is not None, achievements['achievements'])
in_progress = filter(lambda x: 0.0 < x['progress'] < 1.0, achievements['achievements'])
visible = filter(lambda x: not x['hidden'] and x['unlock_date'] is None, achievements['achievements'])
hidden = filter(lambda x: x['hidden'], achievements['achievements'])
print(f'* Completed')
for a in completed:
print(f' - {a["display_name"]} | {a["xp"]}XP | {a["description"]} | Completed on: {a["unlock_date"]}')
print(f'* In progress')
for a in in_progress:
print(f' - {a["display_name"]} | {a["xp"]}XP | {a["description"]} | Progress: {a["progress"] * 100:,.2f}%')
print(f'* Uninitiated')
for a in visible:
print(f' - {a["display_name"]} | {a["xp"]}XP | {a["description"]}')
if args.show_hidden:
print(f'* Undiscovered')
for a in hidden:
print(f' - {a["display_name"]} | {a["xp"]}XP) | {a["description"]}')
return
def main():
# Set output encoding to UTF-8 if not outputting to a terminal
@ -2680,6 +2731,7 @@ def main():
uninstall_parser = subparsers.add_parser('uninstall', help='Uninstall (delete) a game')
verify_parser = subparsers.add_parser('verify', help='Verify a game\'s local files',
aliases=('verify-game',), hide_aliases=True)
achievements_parser = subparsers.add_parser('achievements', help='List achievement status for a given game')
# hidden commands have no help text
get_token_parser = subparsers.add_parser('get-token')
@ -3013,6 +3065,12 @@ def main():
move_parser.add_argument('--skip-move', dest='skip_move', action='store_true',
help='Only change legendary database, do not move files (e.g. if already moved)')
achievements_parser.add_argument('app_name', metavar='<App Name>', help='Name of the app')
achievements_parser.add_argument('--hidden', dest='show_hidden', action='store_true',
help='Show undiscovered achievements (may contain spoilers)')
achievements_parser.add_argument('--json', dest='json', action='store_true',
help='Output information in JSON format')
args, extra = parser.parse_known_args()
if args.version:
@ -3113,6 +3171,8 @@ def main():
cli.crossover_setup(args)
elif args.subparser_name == 'move':
cli.move(args)
elif args.subparser_name == 'achievements':
cli.achievements(args)
except KeyboardInterrupt:
logger.info('Command was aborted via KeyboardInterrupt, cleaning up...')

View file

@ -314,11 +314,11 @@ class LegendaryCore:
return update_info.get('game_wiki', {}).get(app_name, {}).get(sys_platform)
def get_user_achievements(self, app_name: str, update: bool = False):
game = self.get_game(app_name, update_meta=True)
if not self.lgd.achievements or not self.lgd.achievements.get(game.namespace, None) or update:
if not (achievements := self.lgd.achievements):
achievements = {}
def get_user_achievements(self, game: Game, update: bool = False):
if not (achievements := self.lgd.achievements):
achievements = {}
if not achievements or not achievements.get(game.app_name, None) or update:
response = self.egs.get_game_achievements_user(game.namespace)
records = response['data']['PlayerAchievement']['playerAchievementGameRecordsBySandbox']['records']
achievements[game.app_name] = None
@ -326,10 +326,67 @@ class LegendaryCore:
achievements[game.app_name] = records[0]
self.lgd.achievements = achievements
return self.lgd.achievements[app_name]
return self.lgd.achievements[game.app_name]
def get_game_achievements(self, app_name: str):
return self.get_game(app_name).achievements
def get_achievements(self, game: Game, update: bool = False):
game_achievements = game.achievements
if not game_achievements.achievement_sets:
return None
user_achievements = self.get_user_achievements(game, update)
user_unlocked = {}
if user_achievements:
user_unlocked = {
ach['playerAchievement']['achievementName']: ach['playerAchievement'] for ach in
user_achievements['playerAchievements']
}
achievements = {
'total_achievements': game_achievements.total_achievements,
'total_product_xp': game_achievements.total_product_xp,
'achievement_sets': game_achievements.achievement_sets,
'platinum_rarity': game_achievements.platinum_rarity,
'achievements': []
}
achievements.update({
'user_unlocked': user_achievements['totalUnlocked'] if user_achievements else 0,
'user_xp': user_achievements['totalXP'] if user_achievements else 0,
'user_awards': user_achievements['playerAwards'] if user_achievements else [],
})
for item in game_achievements.achievements:
game_ach = item['achievement']
is_unlocked = game_ach['name'] in user_unlocked
_unlocked = False
_progress = 0.0
_unlock_date = None
if is_unlocked:
user_ach = user_unlocked[game_ach['name']]
_unlocked = user_ach['unlocked']
_progress = float(user_ach['progress'])
_unlock_date = user_ach['unlockDate']
_unlock_date = datetime.fromisoformat(_unlock_date[:-1]).replace(
tzinfo=timezone.utc) if _unlock_date != "N/A" else None
data = {
'name': game_ach['name'],
'is_base': game_ach['isBase'],
'hidden': False if is_unlocked else game_ach['hidden'],
'xp': game_ach['XP'],
'unlocked': _unlocked,
'progress': _progress,
'unlock_date': _unlock_date if _unlock_date else None,
'display_name': game_ach['unlockedDisplayName'] if is_unlocked else game_ach['lockedDisplayName'],
'description': game_ach['unlockedDescription'] if is_unlocked else game_ach['lockedDescription'],
'icon_id': game_ach['unlockedIconId'] if is_unlocked else game_ach['lockedIconId'],
'icon_link': game_ach['unlockedIconLink'] if is_unlocked else game_ach['lockedIconLink'],
'tier': game_ach['tier'],
'rarity': game_ach['rarity'],
}
achievements['achievements'].append(data)
return achievements
def get_sdl_data(self, app_name, platform='Windows'):
if platform not in ('Win32', 'Windows'):