Files
ARMSX2/pcsx2/Achievements.h
jpolo1224 05b52de95e Achievements: library-wide RA progress, including games never played
Progress could only ever be shown for a game that had been loaded, because
the core is only able to report on the game it currently has: set sizes for
everything else are not on the device at all. So a game you own but have
never launched showed nothing, which is most of a library.

Fetch it from RetroAchievements instead — and this needs two requests for
an entire library, not one per game:

  - API_GetGameList (i=21 PS2, f=1, h=1) returns every PS2 set's size
    together with its MD5 hashes. Cached on disk for a week; set sizes
    change on the order of months.
  - API_GetUserCompletionProgress returns NumAwarded / NumAwardedHardcore
    / MaxPossible per game id, paginated 500 at a time.

Matching is by disc hash, the only reliable key: RA carries no PS2 serials
and title matching would confuse regional variants and multi-disc sets.
Achievements::GetGameHashForImage computes it without booting, mirroring
the game-list scanner's open/detect/read/close over CDVDapi_Iso. It refuses
while a VM is valid — CDVD is a global, so repointing it mid-session would
swap the disc out from under the running game.

A game with a set and no unlocks now correctly reads 0/N rather than
nothing. Automatic syncs are limited to one a day and only run once a web
API key is present; the RA panel has a manual "Sync library" button that
ignores the interval. The web API key is a separate credential from the
login token, so it is entered once in the panel and trimmed on the way in
(the site's copy button brings whitespace, which would otherwise look like
the feature silently failing).

Also captures progress on every RA sound, so the figure moves as
achievements are earned instead of waiting for the slow poll.

Requested by Isshin.
2026-07-26 12:23:34 -04:00

277 lines
9.2 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "common/Pcsx2Types.h"
#include "Config.h"
#include <functional>
#include <mutex>
#include <span>
#include <string>
#include <utility>
#include <vector>
class Error;
class SaveStateBase;
namespace Achievements
{
enum class LoginRequestReason
{
UserInitiated,
TokenInvalid,
};
/// Acquires the achievements lock. Must be held when accessing any achievement state from another thread.
std::unique_lock<std::recursive_mutex> GetLock();
/// Initializes the RetroAchievments client.
bool Initialize();
/// Updates achievements settings.
void UpdateSettings(const Pcsx2Config::AchievementsOptions& old_config);
/// Resets the internal state of all achievement tracking. Call on system reset.
void ResetClient();
/// Called when the system is being reset. If it returns false, the reset should be aborted.
bool ConfirmSystemReset();
/// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted.
bool Shutdown(bool allow_cancel);
/// Called when the system is being paused and resumed.
void OnVMPaused(bool paused);
/// Called once a frame at vsync time on the CPU thread.
void FrameUpdate();
/// Called when the system is paused, because FrameUpdate() won't be getting called.
void IdleUpdate();
/// Saves/loads state.
void LoadState(std::span<const u8> data);
void SaveState(SaveStateBase& writer);
/// Attempts to log in to RetroAchievements using the specified credentials.
/// If the login is successful, the token returned by the server will be saved.
bool Login(const char* username, const char* password, Error* error);
/// Logs out of RetroAchievements, clearing any credentials.
void Logout();
/// Called when the system changes game, or is booting.
void GameChanged(u32 disc_crc, u32 crc);
/// Play achievement related sounds effects
void PlayAchievementSound(bool is_specific_sound_enabled, const std::string& custom_sound_name, const std::string& default_sound_name);
/// Re-enables hardcode mode if it is enabled in the settings.
bool ResetHardcoreMode(bool is_booting);
/// Forces hardcore mode off until next reset.
void DisableHardcoreMode();
/// Returns the translated title to display for the message box asking the
// user to disable hardcore mode.
const char* GetHardcoreModeDisableTitle();
/// Returns the translated text to display in the message box asking the
/// user to disable hardcore mode.
std::string GetHardcoreModeDisableText(const char* reason);
/// Returns true if hardcore mode is active, and functionality should be restricted.
bool IsHardcoreModeActive();
/// RAIntegration only exists for Windows, so no point checking it on other platforms.
bool IsUsingRAIntegration();
/// Returns true if the achievement system is active. Achievements can be active without a valid client.
bool IsActive();
/// Returns true if RetroAchievements game data has been loaded.
bool HasActiveGame();
/// Returns the RetroAchievements ID for the current game.
u32 GetGameID();
/// Computes the RetroAchievements hash for a disc image WITHOUT booting it, so a frontend can
/// identify a whole library against RA's game list. Returns an empty string if the image cannot
/// be read or carries no PS2 boot ELF.
///
/// Repoints the global CDVD at `image_path`, so it refuses to run while a VM is valid — doing it
/// anyway would pull the disc out from under the running game. Call from a background thread with
/// no VM active, as the game list scanner does.
std::string GetGameHashForImage(const std::string& image_path);
/// Returns true if the current game has any achievements or leaderboards.
bool HasAchievementsOrLeaderboards();
/// Returns true if the current game has any achievements.
bool HasAchievements();
/// Snapshot the current game's achievements as JSON for the in-game
/// overlay's right-side panel. Walks rc_client buckets in display
/// order (active challenge → recently unlocked → unlocked → almost
/// there → locked → unofficial → unsupported). Empty array when no
/// active game / not logged in. Format:
/// {
/// "active": bool, // game has any achievements
/// "loggedIn": bool, // a user is logged in to RA
/// "userName": "string", // display name when loggedIn
/// "items": [
/// { "id": int, "title": "...", "description": "...",
/// "points": int, "unlocked": bool, "bucket": int,
/// "rarity": float, "measuredProgress": "..." }
/// ]
/// }
/// Self-contained — no rcheevos headers needed by the caller.
std::string GetAchievementsAsJSON();
/// Returns true if the current game has any leaderboards.
bool HasLeaderboards();
/// Returns true if the game supports rich presence.
bool HasRichPresence();
/// Returns the current rich presence string.
/// Should be called with the lock held.
const std::string& GetRichPresenceString();
/// Returns the current game icon url.
/// Should be called with the lock held.
const std::string& GetGameIconURL();
/// Returns the RetroAchievements title for the current game.
/// Should be called with the lock held.
const std::string& GetGameTitle();
/// Returns the logged-in user name.
const char* GetLoggedInUserName();
/// Returns the path to the user's profile avatar.
/// Should be called with the lock held.
std::string GetLoggedInUserBadgePath();
/// Clears all cached state used to render the UI.
void ClearUIState();
/// Draws ImGui overlays when not paused.
void DrawGameOverlays();
/// Draws ImGui overlays when paused.
void DrawPauseMenuOverlays();
/// Queries the achievement list, and if no achievements are available, returns false.
bool PrepareAchievementsWindow();
/// Renders the achievement list.
void DrawAchievementsWindow();
/// Queries the leaderboard list, and if no leaderboards are available, returns false.
bool PrepareLeaderboardsWindow();
/// Renders the leaderboard list.
void DrawLeaderboardsWindow();
#ifdef ENABLE_RAINTEGRATION
/// Prevents the internal implementation from being used. Instead, RAIntegration will be
/// called into when achievement-related events occur.
void SwitchToRAIntegration();
namespace RAIntegration
{
void MainWindowChanged(void* new_handle);
void GameChanged();
std::vector<std::tuple<int, std::string, bool>> GetMenuItems();
void ActivateMenuItem(int item);
} // namespace RAIntegration
#endif
struct UserStats
{
std::string username;
std::string display_name;
std::string avatar_path;
u32 points = 0;
u32 softcore_points = 0;
u32 unread_messages = 0;
};
struct GameStats
{
std::string title;
std::string rich_presence;
std::string icon_path;
std::string icon_url;
u32 game_id = 0;
u32 unlocked_achievements = 0;
u32 total_achievements = 0;
u32 unlocked_points = 0;
u32 total_points = 0;
bool has_achievements = false;
bool has_leaderboards = false;
bool has_rich_presence = false;
};
struct AchievementInfo
{
std::string title;
std::string description;
std::string badge_path;
std::string measured_progress;
u32 id = 0;
u32 points = 0;
u32 unlock_time = 0;
u32 state = 0;
u32 category = 0;
u32 bucket = 0;
u32 unlocked = 0;
float measured_percent = 0.0f;
float rarity = 0.0f;
float rarity_hardcore = 0.0f;
};
bool GetCurrentUserStats(UserStats* stats);
bool GetCurrentGameStats(GameStats* stats);
bool GetCurrentAchievementList(std::vector<AchievementInfo>* achievements);
} // namespace Achievements
/// Functions implemented in the frontend.
namespace Host
{
/// Called if the big picture UI requests achievements login, or token login fails.
void OnAchievementsLoginRequested(Achievements::LoginRequestReason reason);
/// Called when achievements login completes.
void OnAchievementsLoginSuccess(const char* display_name, u32 points, u32 sc_points, u32 unread_messages);
/// Called whenever game details or rich presence information is updated.
/// Implementers can assume the lock is held when this is called.
void OnAchievementsRefreshed();
/// Called whenever hardcore mode is toggled.
void OnAchievementsHardcoreModeChanged(bool enabled);
/// Returns true when the platform renders RetroAchievements notifications through its
/// own native UI (for example the iOS SwiftUI toast). When this returns true the shared
/// core routes achievement notifications through OnAchievementNotification instead of
/// the ImGui FullscreenUI overlay. That also keeps the (invisible on such platforms)
/// FullscreenUI from being initialized, which would otherwise add per-frame render cost
/// for nothing.
bool HasNativeAchievementNotifications();
/// Presents a RetroAchievements notification natively. Only invoked when
/// HasNativeAchievementNotifications() returns true; desktop/Android frontends keep
/// using the ImGui overlay and never call this. `key` deduplicates against an in-flight
/// notification of the same kind, `duration` is in seconds, and `badge_path` may be
/// empty. May be called from any thread; implementations must copy the strings before
/// returning.
void OnAchievementNotification(const char* key, float duration, const char* title,
const char* message, const char* badge_path);
} // namespace Host