2024-06-24 01:56:46 +00:00
|
|
|
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
|
2022-12-04 11:03:45 +00:00
|
|
|
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// TODO: Don't poll when booting the game, e.g. Crash Warped freaks out.
|
|
|
|
|
|
|
|
#define IMGUI_DEFINE_MATH_OPERATORS
|
|
|
|
|
2023-08-23 12:06:48 +00:00
|
|
|
#include "achievements.h"
|
2023-10-31 15:32:29 +00:00
|
|
|
#include "achievements_private.h"
|
2023-08-13 06:28:28 +00:00
|
|
|
#include "bios.h"
|
|
|
|
#include "bus.h"
|
|
|
|
#include "cpu_core.h"
|
|
|
|
#include "fullscreen_ui.h"
|
|
|
|
#include "host.h"
|
|
|
|
#include "system.h"
|
|
|
|
|
|
|
|
#include "scmversion/scmversion.h"
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "common/assert.h"
|
2023-09-07 10:13:48 +00:00
|
|
|
#include "common/error.h"
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "common/file_system.h"
|
2024-06-24 01:56:46 +00:00
|
|
|
#include "common/heap_array.h"
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "common/log.h"
|
|
|
|
#include "common/md5_digest.h"
|
|
|
|
#include "common/path.h"
|
2023-09-07 10:13:48 +00:00
|
|
|
#include "common/scoped_guard.h"
|
2023-09-20 13:49:14 +00:00
|
|
|
#include "common/small_string.h"
|
2022-07-11 13:03:29 +00:00
|
|
|
#include "common/string_util.h"
|
2024-08-04 04:29:27 +00:00
|
|
|
#include "common/timer.h"
|
2023-08-13 06:28:28 +00:00
|
|
|
|
|
|
|
#include "util/cd_image.h"
|
2023-11-06 09:59:02 +00:00
|
|
|
#include "util/http_downloader.h"
|
2023-08-13 06:28:28 +00:00
|
|
|
#include "util/imgui_fullscreen.h"
|
2023-08-27 06:00:06 +00:00
|
|
|
#include "util/imgui_manager.h"
|
2023-08-13 06:28:28 +00:00
|
|
|
#include "util/platform_misc.h"
|
|
|
|
#include "util/state_wrapper.h"
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
#include "IconsFontAwesome5.h"
|
2024-01-13 04:34:57 +00:00
|
|
|
#include "IconsPromptFont.h"
|
2023-09-07 10:13:48 +00:00
|
|
|
#include "fmt/format.h"
|
|
|
|
#include "imgui.h"
|
|
|
|
#include "imgui_internal.h"
|
2024-08-04 07:00:06 +00:00
|
|
|
#include "rc_api_runtime.h"
|
2023-09-07 10:13:48 +00:00
|
|
|
#include "rc_client.h"
|
2023-08-13 06:28:28 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
#include <algorithm>
|
2022-09-21 12:02:54 +00:00
|
|
|
#include <atomic>
|
2022-07-11 13:03:29 +00:00
|
|
|
#include <cstdarg>
|
|
|
|
#include <cstdlib>
|
|
|
|
#include <ctime>
|
|
|
|
#include <functional>
|
|
|
|
#include <string>
|
|
|
|
#include <vector>
|
2023-08-13 06:28:28 +00:00
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
Log_SetChannel(Achievements);
|
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
// RA_Interface ends up including windows.h, with its silly macros.
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include "common/windows_headers.h"
|
|
|
|
#endif
|
|
|
|
#include "RA_Interface.h"
|
|
|
|
#endif
|
|
|
|
namespace Achievements {
|
|
|
|
|
2022-09-21 12:54:37 +00:00
|
|
|
static constexpr const char* INFO_SOUND_NAME = "sounds/achievements/message.wav";
|
|
|
|
static constexpr const char* UNLOCK_SOUND_NAME = "sounds/achievements/unlock.wav";
|
|
|
|
static constexpr const char* LBSUBMIT_SOUND_NAME = "sounds/achievements/lbsubmit.wav";
|
2024-01-13 04:39:35 +00:00
|
|
|
static constexpr const char* ACHEIVEMENT_DETAILS_URL_TEMPLATE = "https://retroachievements.org/achievement/{}";
|
2024-08-04 07:00:06 +00:00
|
|
|
static constexpr const char* CACHE_SUBDIRECTORY_NAME = "achievement_images";
|
2022-09-21 12:54:37 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static constexpr u32 LEADERBOARD_NEARBY_ENTRIES_TO_FETCH = 10;
|
|
|
|
static constexpr u32 LEADERBOARD_ALL_FETCH_SIZE = 20;
|
|
|
|
|
|
|
|
static constexpr float LOGIN_NOTIFICATION_TIME = 5.0f;
|
|
|
|
static constexpr float ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME = 5.0f;
|
|
|
|
static constexpr float GAME_COMPLETE_NOTIFICATION_TIME = 20.0f;
|
|
|
|
static constexpr float LEADERBOARD_STARTED_NOTIFICATION_TIME = 3.0f;
|
|
|
|
static constexpr float LEADERBOARD_FAILED_NOTIFICATION_TIME = 3.0f;
|
|
|
|
|
|
|
|
static constexpr float INDICATOR_FADE_IN_TIME = 0.1f;
|
|
|
|
static constexpr float INDICATOR_FADE_OUT_TIME = 0.5f;
|
|
|
|
|
2023-11-06 09:59:16 +00:00
|
|
|
// Some API calls are really slow. Set a longer timeout.
|
|
|
|
static constexpr float SERVER_CALL_TIMEOUT = 60.0f;
|
|
|
|
|
|
|
|
// Chrome uses 10 server calls per domain, seems reasonable.
|
|
|
|
static constexpr u32 MAX_CONCURRENT_SERVER_CALLS = 10;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
namespace {
|
|
|
|
struct LoginWithPasswordParameters
|
|
|
|
{
|
|
|
|
const char* username;
|
|
|
|
Error* error;
|
|
|
|
rc_client_async_handle_t* request;
|
|
|
|
bool result;
|
|
|
|
};
|
|
|
|
struct LeaderboardTrackerIndicator
|
|
|
|
{
|
|
|
|
u32 tracker_id;
|
|
|
|
std::string text;
|
|
|
|
Common::Timer show_hide_time;
|
|
|
|
bool active;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct AchievementChallengeIndicator
|
|
|
|
{
|
|
|
|
const rc_client_achievement_t* achievement;
|
|
|
|
std::string badge_path;
|
|
|
|
Common::Timer show_hide_time;
|
|
|
|
bool active;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct AchievementProgressIndicator
|
|
|
|
{
|
|
|
|
const rc_client_achievement_t* achievement;
|
|
|
|
std::string badge_path;
|
|
|
|
Common::Timer show_hide_time;
|
|
|
|
bool active;
|
|
|
|
};
|
|
|
|
} // namespace
|
|
|
|
|
2024-05-05 10:21:54 +00:00
|
|
|
static void ReportError(std::string_view sv);
|
2023-09-07 10:13:48 +00:00
|
|
|
template<typename... T>
|
|
|
|
static void ReportFmtError(fmt::format_string<T...> fmt, T&&... args);
|
|
|
|
template<typename... T>
|
|
|
|
static void ReportRCError(int err, fmt::format_string<T...> fmt, T&&... args);
|
|
|
|
static void ClearGameInfo();
|
2022-07-11 13:03:29 +00:00
|
|
|
static void ClearGameHash();
|
|
|
|
static std::string GetGameHash(CDImage* image);
|
2023-09-23 02:55:05 +00:00
|
|
|
static void SetHardcoreMode(bool enabled, bool force_display_message);
|
|
|
|
static bool IsLoggedInOrLoggingIn();
|
2024-04-17 14:40:06 +00:00
|
|
|
static bool CanEnableHardcoreMode();
|
2023-09-07 10:13:48 +00:00
|
|
|
static void ShowLoginSuccess(const rc_client_t* client);
|
|
|
|
static void ShowLoginNotification();
|
|
|
|
static void IdentifyGame(const std::string& path, CDImage* image);
|
|
|
|
static void BeginLoadGame();
|
2024-06-24 03:11:13 +00:00
|
|
|
static void BeginChangeDisc();
|
2023-09-07 10:13:48 +00:00
|
|
|
static void UpdateGameSummary();
|
2024-08-04 07:00:06 +00:00
|
|
|
static std::string GetLocalImagePath(const std::string_view image_name, int type);
|
2023-09-07 10:13:48 +00:00
|
|
|
static void DownloadImage(std::string url, std::string cache_filename);
|
|
|
|
|
2023-11-06 09:59:02 +00:00
|
|
|
static bool CreateClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http);
|
|
|
|
static void DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http);
|
2023-09-07 10:13:48 +00:00
|
|
|
static void ClientMessageCallback(const char* message, const rc_client_t* client);
|
|
|
|
static uint32_t ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client);
|
|
|
|
static void ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback, void* callback_data,
|
|
|
|
rc_client_t* client);
|
|
|
|
|
|
|
|
static void ClientEventHandler(const rc_client_event_t* event, rc_client_t* client);
|
|
|
|
static void HandleResetEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleUnlockEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleGameCompleteEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardStartedEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardFailedEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardSubmittedEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardScoreboardEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardTrackerShowEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleAchievementProgressIndicatorUpdateEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleServerErrorEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleServerDisconnectedEvent(const rc_client_event_t* event);
|
|
|
|
static void HandleServerReconnectedEvent(const rc_client_event_t* event);
|
|
|
|
|
|
|
|
static void ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client, void* userdata);
|
|
|
|
static void ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client, void* userdata);
|
|
|
|
static void ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata);
|
|
|
|
|
2023-09-23 15:57:37 +00:00
|
|
|
static void DisplayHardcoreDeferredMessage();
|
2023-09-07 10:13:48 +00:00
|
|
|
static void DisplayAchievementSummary();
|
|
|
|
static void UpdateRichPresence(std::unique_lock<std::recursive_mutex>& lock);
|
|
|
|
|
|
|
|
static void LeaderboardFetchNearbyCallback(int result, const char* error_message,
|
|
|
|
rc_client_leaderboard_entry_list_t* list, rc_client_t* client,
|
|
|
|
void* callback_userdata);
|
|
|
|
static void LeaderboardFetchAllCallback(int result, const char* error_message, rc_client_leaderboard_entry_list_t* list,
|
|
|
|
rc_client_t* client, void* callback_userdata);
|
2023-10-31 15:32:29 +00:00
|
|
|
|
|
|
|
#ifndef __ANDROID__
|
|
|
|
static void DrawAchievement(const rc_client_achievement_t* cheevo);
|
|
|
|
static void DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard);
|
|
|
|
static void DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, bool is_self, float rank_column_width,
|
|
|
|
float name_column_width, float time_column_width, float column_spacing);
|
|
|
|
#endif
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
static bool s_hardcore_mode = false;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
static bool s_using_raintegration = false;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
static std::recursive_mutex s_achievements_mutex;
|
2023-09-07 10:13:48 +00:00
|
|
|
static rc_client_t* s_client;
|
2023-11-06 09:59:02 +00:00
|
|
|
static std::unique_ptr<HTTPDownloader> s_http_downloader;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
static std::string s_game_path;
|
|
|
|
static std::string s_game_hash;
|
|
|
|
static std::string s_game_title;
|
|
|
|
static std::string s_game_icon;
|
2023-09-07 10:13:48 +00:00
|
|
|
static rc_client_user_game_summary_t s_game_summary;
|
|
|
|
static u32 s_game_id = 0;
|
2024-06-24 01:56:46 +00:00
|
|
|
static DynamicHeapArray<u8> s_state_buffer;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static bool s_has_achievements = false;
|
|
|
|
static bool s_has_leaderboards = false;
|
2022-07-11 13:03:29 +00:00
|
|
|
static bool s_has_rich_presence = false;
|
|
|
|
static std::string s_rich_presence_string;
|
2023-09-07 10:13:48 +00:00
|
|
|
static Common::Timer s_rich_presence_poll_time;
|
|
|
|
|
|
|
|
static rc_client_async_handle_t* s_login_request;
|
|
|
|
static rc_client_async_handle_t* s_load_game_request;
|
|
|
|
|
|
|
|
static rc_client_achievement_list_t* s_achievement_list;
|
|
|
|
static rc_client_leaderboard_list_t* s_leaderboard_list;
|
|
|
|
static std::vector<std::pair<const void*, std::string>> s_achievement_badge_paths;
|
|
|
|
static const rc_client_leaderboard_t* s_open_leaderboard = nullptr;
|
|
|
|
static rc_client_async_handle_t* s_leaderboard_fetch_handle = nullptr;
|
|
|
|
static std::vector<rc_client_leaderboard_entry_list_t*> s_leaderboard_entry_lists;
|
|
|
|
static rc_client_leaderboard_entry_list_t* s_leaderboard_nearby_entries;
|
|
|
|
static std::vector<std::pair<const rc_client_leaderboard_entry_t*, std::string>> s_leaderboard_user_icon_paths;
|
|
|
|
static bool s_is_showing_all_leaderboard_entries = false;
|
|
|
|
|
|
|
|
static std::vector<LeaderboardTrackerIndicator> s_active_leaderboard_trackers;
|
|
|
|
static std::vector<AchievementChallengeIndicator> s_active_challenge_indicators;
|
|
|
|
static std::optional<AchievementProgressIndicator> s_active_progress_indicator;
|
2022-07-11 13:03:29 +00:00
|
|
|
} // namespace Achievements
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
std::unique_lock<std::recursive_mutex> Achievements::GetLock()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return std::unique_lock(s_achievements_mutex);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
rc_client_t* Achievements::GetClient()
|
|
|
|
{
|
|
|
|
return s_client;
|
|
|
|
}
|
|
|
|
|
|
|
|
const rc_client_user_game_summary_t& Achievements::GetGameSummary()
|
|
|
|
{
|
|
|
|
return s_game_summary;
|
|
|
|
}
|
|
|
|
|
2024-05-05 10:21:54 +00:00
|
|
|
void Achievements::ReportError(std::string_view sv)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
std::string error = fmt::format("Achievements error: {}", sv);
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG(error.c_str());
|
2023-09-07 10:13:48 +00:00
|
|
|
Host::AddOSDMessage(std::move(error), Host::OSD_CRITICAL_ERROR_DURATION);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
template<typename... T>
|
|
|
|
void Achievements::ReportFmtError(fmt::format_string<T...> fmt, T&&... args)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
TinyString str;
|
|
|
|
fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...));
|
|
|
|
ReportError(str);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
template<typename... T>
|
|
|
|
void Achievements::ReportRCError(int err, fmt::format_string<T...> fmt, T&&... args)
|
2022-08-10 03:04:20 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
TinyString str;
|
|
|
|
fmt::vformat_to(std::back_inserter(str), fmt, fmt::make_format_args(args...));
|
2023-12-13 11:06:15 +00:00
|
|
|
str.append_format("{} ({})", rc_error_str(err), err);
|
2023-09-07 10:13:48 +00:00
|
|
|
ReportError(str);
|
2022-08-10 03:04:20 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
std::string Achievements::GetGameHash(CDImage* image)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
std::string executable_name;
|
|
|
|
std::vector<u8> executable_data;
|
|
|
|
if (!System::ReadExecutableFromImage(image, &executable_name, &executable_data))
|
|
|
|
return {};
|
|
|
|
|
2024-07-14 11:20:56 +00:00
|
|
|
BIOS::PSEXEHeader header = {};
|
2023-09-07 10:13:48 +00:00
|
|
|
if (executable_data.size() >= sizeof(header))
|
|
|
|
std::memcpy(&header, executable_data.data(), sizeof(header));
|
2024-07-28 06:16:05 +00:00
|
|
|
if (!BIOS::IsValidPSExeHeader(header, executable_data.size()))
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("PS-EXE header is invalid in '{}' ({} bytes)", executable_name, executable_data.size());
|
2023-09-07 10:13:48 +00:00
|
|
|
return {};
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// See rcheevos hash.c - rc_hash_psx().
|
|
|
|
const u32 MAX_HASH_SIZE = 64 * 1024 * 1024;
|
2024-07-14 11:20:56 +00:00
|
|
|
const u32 hash_size =
|
|
|
|
std::min(std::min<u32>(sizeof(header) + header.file_size, MAX_HASH_SIZE), static_cast<u32>(executable_data.size()));
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
MD5Digest digest;
|
|
|
|
digest.Update(executable_name.c_str(), static_cast<u32>(executable_name.size()));
|
|
|
|
if (hash_size > 0)
|
2024-07-18 07:33:15 +00:00
|
|
|
digest.Update(executable_data);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
u8 hash[16];
|
|
|
|
digest.Final(hash);
|
|
|
|
|
2023-09-23 02:55:05 +00:00
|
|
|
const std::string hash_str =
|
|
|
|
fmt::format("{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
|
|
|
hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7], hash[8], hash[9], hash[10],
|
|
|
|
hash[11], hash[12], hash[13], hash[14], hash[15]);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Hash for '{}' ({} bytes, {} bytes hashed): {}", executable_name, executable_data.size(), hash_size,
|
|
|
|
hash_str);
|
2023-09-07 10:13:48 +00:00
|
|
|
return hash_str;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2024-08-04 07:00:06 +00:00
|
|
|
std::string Achievements::GetLocalImagePath(const std::string_view image_name, int type)
|
|
|
|
{
|
|
|
|
std::string_view prefix;
|
|
|
|
switch (type)
|
|
|
|
{
|
|
|
|
case RC_IMAGE_TYPE_GAME:
|
|
|
|
prefix = "image"; // https://media.retroachievements.org/Images/{}.png
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_IMAGE_TYPE_USER:
|
|
|
|
prefix = "user"; // https://media.retroachievements.org/UserPic/{}.png
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_IMAGE_TYPE_ACHIEVEMENT: // https://media.retroachievements.org/Badge/{}.png
|
|
|
|
case RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED:
|
|
|
|
default:
|
|
|
|
prefix = "badge";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string ret;
|
|
|
|
if (!image_name.empty())
|
|
|
|
{
|
|
|
|
ret = fmt::format("{}" FS_OSPATH_SEPARATOR_STR "{}" FS_OSPATH_SEPARATOR_STR "{}_{}.png", EmuFolders::Cache,
|
|
|
|
CACHE_SUBDIRECTORY_NAME, prefix, Path::SanitizeFileName(image_name));
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DownloadImage(std::string url, std::string cache_filename)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-11-26 12:11:18 +00:00
|
|
|
auto callback = [cache_filename](s32 status_code, const std::string& content_type,
|
|
|
|
HTTPDownloader::Request::Data data) {
|
2023-11-06 09:59:02 +00:00
|
|
|
if (status_code != HTTPDownloader::HTTP_STATUS_OK)
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!FileSystem::WriteBinaryFile(cache_filename.c_str(), data.data(), data.size()))
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("Failed to write badge image to '{}'", cache_filename);
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::InvalidateCachedTexture(cache_filename);
|
|
|
|
};
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_http_downloader->CreateRequest(std::move(url), std::move(callback));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::IsActive()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2023-09-07 10:13:48 +00:00
|
|
|
return (s_client != nullptr) || s_using_raintegration;
|
2022-07-11 13:03:29 +00:00
|
|
|
#else
|
2023-09-07 10:13:48 +00:00
|
|
|
return (s_client != nullptr);
|
2022-07-11 13:03:29 +00:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::IsHardcoreModeActive()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
return RA_HardcoreModeIsActive() != 0;
|
|
|
|
#endif
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_hardcore_mode;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::HasActiveGame()
|
2022-09-21 13:45:24 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_game_id != 0;
|
2022-09-21 13:45:24 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
u32 Achievements::GetGameID()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_game_id;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::HasAchievementsOrLeaderboards()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_has_achievements || s_has_leaderboards;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
bool Achievements::HasAchievements()
|
|
|
|
{
|
|
|
|
return s_has_achievements;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::HasLeaderboards()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_has_leaderboards;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::HasRichPresence()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_has_rich_presence;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const std::string& Achievements::GetGameTitle()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_game_title;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
const std::string& Achievements::GetGameIconPath()
|
|
|
|
{
|
|
|
|
return s_game_icon;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const std::string& Achievements::GetRichPresenceString()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
return s_rich_presence_string;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::Initialize()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
if (IsUsingRAIntegration())
|
2023-09-07 10:13:48 +00:00
|
|
|
return true;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
AssertMsg(g_settings.achievements_enabled, "Achievements are enabled");
|
2023-09-07 10:13:48 +00:00
|
|
|
Assert(!s_client && !s_http_downloader);
|
|
|
|
|
|
|
|
if (!CreateClient(&s_client, &s_http_downloader))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Hardcore starts off. We enable it on first boot.
|
|
|
|
s_hardcore_mode = false;
|
|
|
|
|
|
|
|
rc_client_set_event_handler(s_client, ClientEventHandler);
|
|
|
|
|
|
|
|
rc_client_set_hardcore_enabled(s_client, s_hardcore_mode);
|
2023-09-18 12:29:47 +00:00
|
|
|
rc_client_set_encore_mode_enabled(s_client, g_settings.achievements_encore_mode);
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_set_unofficial_enabled(s_client, g_settings.achievements_unofficial_test_mode);
|
|
|
|
rc_client_set_spectator_mode_enabled(s_client, g_settings.achievements_spectator_mode);
|
|
|
|
|
|
|
|
// Begin disc identification early, before the login finishes.
|
|
|
|
if (System::IsValid())
|
|
|
|
IdentifyGame(System::GetDiscPath(), nullptr);
|
|
|
|
|
|
|
|
std::string username = Host::GetBaseStringSettingValue("Cheevos", "Username");
|
|
|
|
std::string api_token = Host::GetBaseStringSettingValue("Cheevos", "Token");
|
|
|
|
if (!username.empty() && !api_token.empty())
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Attempting login with user '{}'...", username);
|
2023-09-07 10:13:48 +00:00
|
|
|
s_login_request = rc_client_begin_login_with_token(s_client, username.c_str(), api_token.c_str(),
|
|
|
|
ClientLoginWithTokenCallback, nullptr);
|
|
|
|
}
|
|
|
|
|
2023-09-23 15:57:37 +00:00
|
|
|
// Hardcore mode isn't enabled when achievements first starts, if a game is already running.
|
|
|
|
if (System::IsValid() && IsLoggedInOrLoggingIn() && g_settings.achievements_hardcore_mode)
|
|
|
|
DisplayHardcoreDeferredMessage();
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
return true;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-11-06 09:59:02 +00:00
|
|
|
bool Achievements::CreateClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
2023-11-24 05:54:43 +00:00
|
|
|
*http = HTTPDownloader::Create(Host::GetHTTPUserAgent());
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!*http)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
Host::ReportErrorAsync("Achievements Error", "Failed to create HTTPDownloader, cannot use achievements");
|
2023-09-07 10:13:48 +00:00
|
|
|
return false;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-11-06 09:59:16 +00:00
|
|
|
(*http)->SetTimeout(SERVER_CALL_TIMEOUT);
|
|
|
|
(*http)->SetMaxActiveRequests(MAX_CONCURRENT_SERVER_CALLS);
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_t* new_client = rc_client_create(ClientReadMemory, ClientServerCall);
|
|
|
|
if (!new_client)
|
|
|
|
{
|
|
|
|
Host::ReportErrorAsync("Achievements Error", "rc_client_create() failed, cannot use achievements");
|
|
|
|
http->reset();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef _DEBUG
|
|
|
|
rc_client_enable_logging(new_client, RC_CLIENT_LOG_LEVEL_VERBOSE, ClientMessageCallback);
|
|
|
|
#else
|
|
|
|
rc_client_enable_logging(new_client, RC_CLIENT_LOG_LEVEL_INFO, ClientMessageCallback);
|
|
|
|
#endif
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_set_userdata(new_client, http->get());
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
*client = new_client;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-11-06 09:59:02 +00:00
|
|
|
void Achievements::DestroyClient(rc_client_t** client, std::unique_ptr<HTTPDownloader>* http)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
(*http)->WaitForAllRequests();
|
|
|
|
|
|
|
|
rc_client_destroy(*client);
|
|
|
|
*client = nullptr;
|
|
|
|
|
|
|
|
http->reset();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::UpdateSettings(const Settings& old_config)
|
|
|
|
{
|
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!g_settings.achievements_enabled)
|
|
|
|
{
|
|
|
|
// we're done here
|
2023-09-07 10:13:48 +00:00
|
|
|
Shutdown(false);
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!IsActive())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
// we just got enabled
|
|
|
|
Initialize();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (g_settings.achievements_hardcore_mode != old_config.achievements_hardcore_mode)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
// Hardcore mode can only be enabled through reset (ResetChallengeMode()).
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_hardcore_mode && !g_settings.achievements_hardcore_mode)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-04-17 14:40:06 +00:00
|
|
|
ResetHardcoreMode(false);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
else if (!s_hardcore_mode && g_settings.achievements_hardcore_mode)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-23 15:57:37 +00:00
|
|
|
if (HasActiveGame())
|
|
|
|
DisplayHardcoreDeferredMessage();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// These cannot be modified while a game is loaded, so just toss state and reload.
|
2023-09-18 12:29:47 +00:00
|
|
|
if (HasActiveGame())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-18 12:29:47 +00:00
|
|
|
if (g_settings.achievements_encore_mode != old_config.achievements_encore_mode ||
|
|
|
|
g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode ||
|
|
|
|
g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode)
|
|
|
|
{
|
|
|
|
Shutdown(false);
|
|
|
|
Initialize();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (g_settings.achievements_encore_mode != old_config.achievements_encore_mode)
|
|
|
|
rc_client_set_encore_mode_enabled(s_client, g_settings.achievements_encore_mode);
|
|
|
|
if (g_settings.achievements_spectator_mode != old_config.achievements_spectator_mode)
|
|
|
|
rc_client_set_spectator_mode_enabled(s_client, g_settings.achievements_spectator_mode);
|
|
|
|
if (g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode)
|
|
|
|
rc_client_set_unofficial_enabled(s_client, g_settings.achievements_unofficial_test_mode);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::Shutdown(bool allow_cancel)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
if (System::IsValid() && allow_cancel && !RA_ConfirmLoadNewRom(true))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
RA_SetPaused(false);
|
|
|
|
RA_ActivateGame(0);
|
|
|
|
return true;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
#endif
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!IsActive())
|
|
|
|
return true;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = GetLock();
|
|
|
|
Assert(s_client && s_http_downloader);
|
|
|
|
|
|
|
|
ClearGameInfo();
|
|
|
|
ClearGameHash();
|
2023-09-18 12:29:47 +00:00
|
|
|
DisableHardcoreMode();
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (s_load_game_request)
|
|
|
|
{
|
|
|
|
rc_client_abort_async(s_client, s_load_game_request);
|
|
|
|
s_load_game_request = nullptr;
|
|
|
|
}
|
|
|
|
if (s_login_request)
|
|
|
|
{
|
|
|
|
rc_client_abort_async(s_client, s_login_request);
|
|
|
|
s_login_request = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
s_hardcore_mode = false;
|
|
|
|
DestroyClient(&s_client, &s_http_downloader);
|
|
|
|
|
|
|
|
Host::OnAchievementsRefreshed();
|
2022-07-11 13:03:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ClientMessageCallback(const char* message, const rc_client_t* client)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG(message);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t Achievements::ClientReadMemory(uint32_t address, uint8_t* buffer, uint32_t num_bytes, rc_client_t* client)
|
|
|
|
{
|
2024-06-24 02:20:01 +00:00
|
|
|
if ((address + num_bytes) > 0x200400U) [[unlikely]]
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
const u8* src = (address >= 0x200000U) ? CPU::g_state.scratchpad.data() : Bus::g_ram;
|
|
|
|
const u32 offset = (address & Bus::RAM_2MB_MASK); // size guarded by check above
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
switch (num_bytes)
|
|
|
|
{
|
|
|
|
case 1:
|
2024-06-24 02:20:01 +00:00
|
|
|
std::memcpy(buffer, &src[offset], 1);
|
|
|
|
break;
|
2023-09-07 10:13:48 +00:00
|
|
|
case 2:
|
2024-06-24 02:20:01 +00:00
|
|
|
std::memcpy(buffer, &src[offset], 2);
|
|
|
|
break;
|
2023-09-07 10:13:48 +00:00
|
|
|
case 4:
|
2024-06-24 02:20:01 +00:00
|
|
|
std::memcpy(buffer, &src[offset], 4);
|
|
|
|
break;
|
2023-09-07 10:13:48 +00:00
|
|
|
default:
|
2024-06-24 02:20:01 +00:00
|
|
|
[[unlikely]] std::memcpy(buffer, &src[offset], num_bytes);
|
|
|
|
break;
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2024-06-24 02:20:01 +00:00
|
|
|
|
|
|
|
return num_bytes;
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::ClientServerCall(const rc_api_request_t* request, rc_client_server_callback_t callback,
|
|
|
|
void* callback_data, rc_client_t* client)
|
|
|
|
{
|
2023-11-26 12:11:18 +00:00
|
|
|
HTTPDownloader::Request::Callback hd_callback =
|
|
|
|
[callback, callback_data](s32 status_code, const std::string& content_type, HTTPDownloader::Request::Data data) {
|
|
|
|
rc_api_server_response_t rr;
|
|
|
|
rr.http_status_code = (status_code <= 0) ? (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED ?
|
|
|
|
RC_API_SERVER_RESPONSE_CLIENT_ERROR :
|
|
|
|
RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR) :
|
|
|
|
status_code;
|
|
|
|
rr.body_length = data.size();
|
|
|
|
rr.body = reinterpret_cast<const char*>(data.data());
|
|
|
|
|
|
|
|
callback(&rr, callback_data);
|
|
|
|
};
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-11-06 09:59:02 +00:00
|
|
|
HTTPDownloader* http = static_cast<HTTPDownloader*>(rc_client_get_userdata(client));
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
// TODO: Content-type for post
|
|
|
|
if (request->post_data)
|
|
|
|
{
|
|
|
|
// const auto pd = std::string_view(request->post_data);
|
2023-09-20 14:11:55 +00:00
|
|
|
// Log_DevFmt("Server POST: {}", pd.substr(0, std::min<size_t>(pd.length(), 10)));
|
2023-09-07 10:13:48 +00:00
|
|
|
http->CreatePostRequest(request->url, request->post_data, std::move(hd_callback));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
http->CreateRequest(request->url, std::move(hd_callback));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::IdleUpdate()
|
|
|
|
{
|
|
|
|
if (!IsActive())
|
|
|
|
return;
|
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2023-09-07 10:13:48 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
return;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
const auto lock = GetLock();
|
|
|
|
|
|
|
|
s_http_downloader->PollRequests();
|
|
|
|
rc_client_idle(s_client);
|
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
bool Achievements::NeedsIdleUpdate()
|
|
|
|
{
|
|
|
|
if (!IsActive())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const auto lock = GetLock();
|
|
|
|
return (s_http_downloader && s_http_downloader->HasAnyRequests());
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::FrameUpdate()
|
|
|
|
{
|
|
|
|
if (!IsActive())
|
|
|
|
return;
|
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2023-09-07 10:13:48 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
|
|
|
RA_DoAchievementsFrame();
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = GetLock();
|
|
|
|
|
|
|
|
s_http_downloader->PollRequests();
|
|
|
|
rc_client_do_frame(s_client);
|
|
|
|
|
|
|
|
UpdateRichPresence(lock);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ClientEventHandler(const rc_client_event_t* event, rc_client_t* client)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
switch (event->type)
|
|
|
|
{
|
|
|
|
case RC_CLIENT_EVENT_RESET:
|
|
|
|
HandleResetEvent(event);
|
|
|
|
break;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
case RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED:
|
|
|
|
HandleUnlockEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_GAME_COMPLETED:
|
|
|
|
HandleGameCompleteEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_STARTED:
|
|
|
|
HandleLeaderboardStartedEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_FAILED:
|
|
|
|
HandleLeaderboardFailedEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED:
|
|
|
|
HandleLeaderboardSubmittedEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD:
|
|
|
|
HandleLeaderboardScoreboardEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW:
|
|
|
|
HandleLeaderboardTrackerShowEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE:
|
|
|
|
HandleLeaderboardTrackerHideEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE:
|
|
|
|
HandleLeaderboardTrackerUpdateEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
|
|
|
|
HandleAchievementChallengeIndicatorShowEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
|
|
|
|
HandleAchievementChallengeIndicatorHideEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW:
|
|
|
|
HandleAchievementProgressIndicatorShowEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE:
|
|
|
|
HandleAchievementProgressIndicatorHideEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE:
|
|
|
|
HandleAchievementProgressIndicatorUpdateEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_SERVER_ERROR:
|
|
|
|
HandleServerErrorEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_DISCONNECTED:
|
|
|
|
HandleServerDisconnectedEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_EVENT_RECONNECTED:
|
|
|
|
HandleServerReconnectedEvent(event);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2024-05-23 10:55:28 +00:00
|
|
|
[[unlikely]] ERROR_LOG("Unhandled event: {}", event->type);
|
2023-09-07 10:13:48 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::UpdateGameSummary()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_get_user_game_summary(s_client, &s_game_summary);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::UpdateRichPresence(std::unique_lock<std::recursive_mutex>& lock)
|
|
|
|
{
|
|
|
|
// Limit rich presence updates to once per second, since it could change per frame.
|
|
|
|
if (!s_has_rich_presence || !s_rich_presence_poll_time.ResetIfSecondsPassed(1.0))
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
char buffer[512];
|
|
|
|
const size_t res = rc_client_get_rich_presence_message(s_client, buffer, std::size(buffer));
|
|
|
|
const std::string_view sv(buffer, res);
|
|
|
|
if (s_rich_presence_string == sv)
|
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_rich_presence_string.assign(sv);
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Rich presence updated: {}", s_rich_presence_string);
|
2023-09-07 10:13:48 +00:00
|
|
|
Host::OnAchievementsRefreshed();
|
|
|
|
|
|
|
|
lock.unlock();
|
2024-07-04 04:40:16 +00:00
|
|
|
System::UpdateRichPresence(false);
|
2023-09-07 10:13:48 +00:00
|
|
|
lock.lock();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::GameChanged(const std::string& path, CDImage* image)
|
|
|
|
{
|
|
|
|
std::unique_lock lock(s_achievements_mutex);
|
|
|
|
|
|
|
|
if (!IsActive())
|
|
|
|
return;
|
|
|
|
|
|
|
|
IdentifyGame(path, image);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::IdentifyGame(const std::string& path, CDImage* image)
|
|
|
|
{
|
|
|
|
if (s_game_path == path)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
WARNING_LOG("Game path is unchanged.");
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
std::unique_ptr<CDImage> temp_image;
|
|
|
|
if (!path.empty() && (!image || (g_settings.achievements_use_first_disc_from_playlist && image->HasSubImages() &&
|
|
|
|
image->GetCurrentSubImage() != 0)))
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
temp_image = CDImage::Open(path.c_str(), g_settings.cdrom_load_image_patches, nullptr);
|
|
|
|
image = temp_image.get();
|
|
|
|
if (!temp_image)
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("Failed to open temporary CD image '{}'", path);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
std::string game_hash;
|
|
|
|
if (image)
|
|
|
|
game_hash = GetGameHash(image);
|
2023-09-18 12:29:47 +00:00
|
|
|
|
|
|
|
if (s_game_hash == game_hash)
|
|
|
|
{
|
|
|
|
// only the path has changed - different format/save state/etc.
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Detected path change from '{}' to '{}'", s_game_path, path);
|
2023-09-18 12:29:47 +00:00
|
|
|
s_game_path = path;
|
|
|
|
return;
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ClearGameHash();
|
|
|
|
s_game_path = path;
|
|
|
|
s_game_hash = std::move(game_hash);
|
2024-06-24 01:56:46 +00:00
|
|
|
s_state_buffer.deallocate();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
RAIntegration::GameChanged();
|
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// shouldn't have a load game request when we're not logged in.
|
2023-09-23 02:55:05 +00:00
|
|
|
Assert(IsLoggedInOrLoggingIn() || !s_load_game_request);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// bail out if we're not logged in, just save the hash
|
2023-09-23 02:55:05 +00:00
|
|
|
if (!IsLoggedInOrLoggingIn())
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Skipping load game because we're not logged in.");
|
2023-09-18 12:29:47 +00:00
|
|
|
DisableHardcoreMode();
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-06-24 03:11:13 +00:00
|
|
|
if (!rc_client_is_game_loaded(s_client))
|
|
|
|
BeginLoadGame();
|
|
|
|
else
|
|
|
|
BeginChangeDisc();
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::BeginLoadGame()
|
2024-06-24 03:11:13 +00:00
|
|
|
{
|
|
|
|
ClearGameInfo();
|
|
|
|
|
|
|
|
if (s_game_hash.empty())
|
|
|
|
{
|
|
|
|
// when we're booting the bios, this will fail
|
|
|
|
if (!s_game_path.empty())
|
|
|
|
{
|
|
|
|
Host::AddKeyedOSDMessage(
|
|
|
|
"retroachievements_disc_read_failed",
|
|
|
|
TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."),
|
|
|
|
Host::OSD_ERROR_DURATION);
|
|
|
|
}
|
|
|
|
|
|
|
|
DisableHardcoreMode();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
s_load_game_request = rc_client_begin_load_game(s_client, s_game_hash.c_str(), ClientLoadGameCallback, nullptr);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::BeginChangeDisc()
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
// cancel previous requests
|
|
|
|
if (s_load_game_request)
|
|
|
|
{
|
|
|
|
rc_client_abort_async(s_client, s_load_game_request);
|
|
|
|
s_load_game_request = nullptr;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_game_hash.empty())
|
|
|
|
{
|
|
|
|
// when we're booting the bios, this will fail
|
|
|
|
if (!s_game_path.empty())
|
|
|
|
{
|
2024-06-24 03:11:13 +00:00
|
|
|
Host::AddKeyedOSDMessage(
|
|
|
|
"retroachievements_disc_read_failed",
|
|
|
|
TRANSLATE_STR("Achievements", "Failed to read executable from disc. Achievements disabled."),
|
|
|
|
Host::OSD_ERROR_DURATION);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
2024-06-24 03:11:13 +00:00
|
|
|
ClearGameInfo();
|
2023-09-18 12:29:47 +00:00
|
|
|
DisableHardcoreMode();
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-06-24 03:11:13 +00:00
|
|
|
s_load_game_request = rc_client_begin_change_media_from_hash(s_client, s_game_hash.c_str(), ClientLoadGameCallback,
|
|
|
|
reinterpret_cast<void*>(static_cast<uintptr_t>(1)));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ClientLoadGameCallback(int result, const char* error_message, rc_client_t* client, void* userdata)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-06-24 03:11:13 +00:00
|
|
|
const bool was_disc_change = (userdata != nullptr);
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_load_game_request = nullptr;
|
2024-06-24 01:56:46 +00:00
|
|
|
s_state_buffer.deallocate();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
if (result == RC_NO_GAME_LOADED)
|
|
|
|
{
|
|
|
|
// Unknown game.
|
2024-05-25 05:45:17 +00:00
|
|
|
INFO_LOG("Unknown game '{}', disabling achievements.", s_game_hash);
|
2024-06-24 03:11:13 +00:00
|
|
|
if (was_disc_change)
|
|
|
|
ClearGameInfo();
|
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
DisableHardcoreMode();
|
|
|
|
return;
|
|
|
|
}
|
2023-09-23 15:57:37 +00:00
|
|
|
else if (result == RC_LOGIN_REQUIRED)
|
|
|
|
{
|
|
|
|
// We would've asked to re-authenticate, so leave HC on for now.
|
|
|
|
// Once we've done so, we'll reload the game.
|
|
|
|
return;
|
|
|
|
}
|
2023-09-18 12:29:47 +00:00
|
|
|
else if (result != RC_OK)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
ReportFmtError("Loading game failed: {}", error_message);
|
2024-06-24 03:11:13 +00:00
|
|
|
if (was_disc_change)
|
|
|
|
ClearGameInfo();
|
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
DisableHardcoreMode();
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
|
|
|
}
|
2024-06-24 03:11:13 +00:00
|
|
|
else if (result == RC_HARDCORE_DISABLED)
|
|
|
|
{
|
|
|
|
if (error_message)
|
|
|
|
ReportError(error_message);
|
|
|
|
|
|
|
|
DisableHardcoreMode();
|
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
const rc_client_game_t* info = rc_client_get_game_info(s_client);
|
|
|
|
if (!info)
|
|
|
|
{
|
|
|
|
ReportError("rc_client_get_game_info() returned NULL");
|
2024-06-24 03:11:13 +00:00
|
|
|
if (was_disc_change)
|
|
|
|
ClearGameInfo();
|
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
DisableHardcoreMode();
|
2023-09-07 10:13:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-04-17 14:40:06 +00:00
|
|
|
const bool has_achievements = rc_client_has_achievements(client);
|
|
|
|
const bool has_leaderboards = rc_client_has_leaderboards(client);
|
|
|
|
|
2024-06-24 03:11:13 +00:00
|
|
|
// Only display summary if the game title has changed across discs.
|
|
|
|
const bool display_summary = (s_game_id != info->id || s_game_title != info->title);
|
|
|
|
|
2024-04-17 14:40:06 +00:00
|
|
|
// If the game has a RetroAchievements entry but no achievements or leaderboards,
|
|
|
|
// enforcing hardcore mode is pointless.
|
|
|
|
if (!has_achievements && !has_leaderboards)
|
|
|
|
DisableHardcoreMode();
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// We should have matched hardcore mode state.
|
|
|
|
Assert(s_hardcore_mode == (rc_client_get_hardcore_enabled(client) != 0));
|
|
|
|
|
|
|
|
s_game_id = info->id;
|
|
|
|
s_game_title = info->title;
|
2024-04-17 14:40:06 +00:00
|
|
|
s_has_achievements = has_achievements;
|
|
|
|
s_has_leaderboards = has_leaderboards;
|
2023-09-07 10:13:48 +00:00
|
|
|
s_has_rich_presence = rc_client_has_rich_presence(client);
|
|
|
|
|
|
|
|
// ensure fullscreen UI is ready for notifications
|
2024-06-24 03:11:13 +00:00
|
|
|
if (display_summary)
|
|
|
|
FullscreenUI::Initialize();
|
2023-09-07 10:13:48 +00:00
|
|
|
|
2024-08-04 07:00:06 +00:00
|
|
|
s_game_icon = GetLocalImagePath(info->badge_name, RC_IMAGE_TYPE_GAME);
|
|
|
|
if (!s_game_icon.empty() && !FileSystem::FileExists(s_game_icon.c_str()))
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
2024-08-04 07:00:06 +00:00
|
|
|
char buf[512];
|
|
|
|
if (int err = rc_client_game_get_image_url(info, buf, std::size(buf)); err == RC_OK)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
2024-08-04 07:00:06 +00:00
|
|
|
DownloadImage(buf, s_game_icon);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ReportRCError(err, "rc_client_game_get_image_url() failed: ");
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdateGameSummary();
|
2024-06-24 03:11:13 +00:00
|
|
|
if (display_summary)
|
|
|
|
DisplayAchievementSummary();
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
Host::OnAchievementsRefreshed();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::ClearGameInfo()
|
|
|
|
{
|
|
|
|
ClearUIState();
|
2023-09-18 12:29:47 +00:00
|
|
|
|
|
|
|
if (s_load_game_request)
|
|
|
|
{
|
|
|
|
rc_client_abort_async(s_client, s_load_game_request);
|
|
|
|
s_load_game_request = nullptr;
|
|
|
|
}
|
|
|
|
rc_client_unload_game(s_client);
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_active_leaderboard_trackers = {};
|
|
|
|
s_active_challenge_indicators = {};
|
|
|
|
s_active_progress_indicator.reset();
|
|
|
|
s_game_id = 0;
|
|
|
|
s_game_title = {};
|
|
|
|
s_game_icon = {};
|
2024-06-24 01:56:46 +00:00
|
|
|
s_state_buffer.deallocate();
|
2023-09-07 10:13:48 +00:00
|
|
|
s_has_achievements = false;
|
|
|
|
s_has_leaderboards = false;
|
|
|
|
s_has_rich_presence = false;
|
|
|
|
s_rich_presence_string = {};
|
|
|
|
s_game_summary = {};
|
2023-09-18 12:29:47 +00:00
|
|
|
|
|
|
|
Host::OnAchievementsRefreshed();
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::ClearGameHash()
|
|
|
|
{
|
|
|
|
s_game_path = {};
|
|
|
|
std::string().swap(s_game_hash);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::DisplayAchievementSummary()
|
|
|
|
{
|
|
|
|
if (g_settings.achievements_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
std::string title;
|
|
|
|
if (IsHardcoreModeActive())
|
|
|
|
title = fmt::format(TRANSLATE_FS("Achievements", "{} (Hardcore Mode)"), s_game_title);
|
|
|
|
else
|
|
|
|
title = s_game_title;
|
|
|
|
|
|
|
|
std::string summary;
|
|
|
|
if (s_game_summary.num_core_achievements > 0)
|
|
|
|
{
|
2023-10-31 15:32:29 +00:00
|
|
|
summary = fmt::format(
|
2024-05-12 15:46:45 +00:00
|
|
|
TRANSLATE_FS("Achievements", "{0}, {1}."),
|
|
|
|
SmallString::from_format(TRANSLATE_PLURAL_FS("Achievements", "You have unlocked {} of %n achievements",
|
|
|
|
"Achievement popup", s_game_summary.num_core_achievements),
|
|
|
|
s_game_summary.num_unlocked_achievements),
|
|
|
|
SmallString::from_format(TRANSLATE_PLURAL_FS("Achievements", "and earned {} of %n points", "Achievement popup",
|
|
|
|
s_game_summary.points_core),
|
|
|
|
s_game_summary.points_unlocked));
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
summary = TRANSLATE_STR("Achievements", "This game has no achievements.");
|
|
|
|
}
|
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification("achievement_summary", ACHIEVEMENT_SUMMARY_NOTIFICATION_TIME, std::move(title),
|
|
|
|
std::move(summary), s_game_icon);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Technically not going through the resource API, but since we're passing this to something else, we can't.
|
|
|
|
if (g_settings.achievements_sound_effects)
|
2024-01-10 03:35:06 +00:00
|
|
|
PlatformMisc::PlaySoundAsync(EmuFolders::GetOverridableResourcePath(INFO_SOUND_NAME).c_str());
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
2023-09-23 15:57:37 +00:00
|
|
|
void Achievements::DisplayHardcoreDeferredMessage()
|
|
|
|
{
|
|
|
|
if (g_settings.achievements_hardcore_mode && !s_hardcore_mode && System::IsValid() && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
ImGuiFullscreen::ShowToast(std::string(),
|
|
|
|
TRANSLATE_STR("Achievements", "Hardcore mode will be enabled on system reset."),
|
|
|
|
Host::OSD_WARNING_DURATION);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::HandleResetEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
// We handle system resets ourselves, but still need to reset the client's state.
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Resetting runtime due to reset event");
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_reset(s_client);
|
|
|
|
|
|
|
|
if (HasActiveGame())
|
|
|
|
UpdateGameSummary();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleUnlockEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
const rc_client_achievement_t* cheevo = event->achievement;
|
|
|
|
DebugAssert(cheevo);
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Achievement {} ({}) for game {} unlocked", cheevo->title, cheevo->id, s_game_id);
|
2023-09-07 10:13:48 +00:00
|
|
|
UpdateGameSummary();
|
|
|
|
|
|
|
|
if (g_settings.achievements_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
std::string title;
|
|
|
|
if (cheevo->category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL)
|
|
|
|
title = fmt::format(TRANSLATE_FS("Achievements", "{} (Unofficial)"), cheevo->title);
|
|
|
|
else
|
|
|
|
title = cheevo->title;
|
|
|
|
|
|
|
|
std::string badge_path = GetAchievementBadgePath(cheevo, cheevo->state);
|
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification(fmt::format("achievement_unlock_{}", cheevo->id),
|
2023-09-18 12:29:47 +00:00
|
|
|
static_cast<float>(g_settings.achievements_notification_duration),
|
|
|
|
std::move(title), cheevo->description, std::move(badge_path));
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (g_settings.achievements_sound_effects)
|
2024-01-10 03:35:06 +00:00
|
|
|
PlatformMisc::PlaySoundAsync(EmuFolders::GetOverridableResourcePath(UNLOCK_SOUND_NAME).c_str());
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Game {} complete", s_game_id);
|
2023-09-07 10:13:48 +00:00
|
|
|
UpdateGameSummary();
|
|
|
|
|
|
|
|
if (g_settings.achievements_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
std::string title = fmt::format(TRANSLATE_FS("Achievements", "Mastered {}"), s_game_title);
|
2024-06-13 08:30:16 +00:00
|
|
|
std::string message =
|
|
|
|
fmt::format(TRANSLATE_FS("Achievements", "{0}, {1}"),
|
|
|
|
TRANSLATE_PLURAL_STR("Achievements", "%n achievements", "Mastery popup",
|
|
|
|
s_game_summary.num_unlocked_achievements),
|
|
|
|
TRANSLATE_PLURAL_STR("Achievements", "%n points", "Mastery popup", s_game_summary.points_unlocked));
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification("achievement_mastery", GAME_COMPLETE_NOTIFICATION_TIME, std::move(title),
|
|
|
|
std::move(message), s_game_icon);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Leaderboard {} ({}) started", event->leaderboard->id, event->leaderboard->title);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
std::string title = event->leaderboard->title;
|
|
|
|
std::string message = TRANSLATE_STR("Achievements", "Leaderboard attempt started.");
|
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
|
|
|
LEADERBOARD_STARTED_NOTIFICATION_TIME, std::move(title), std::move(message),
|
|
|
|
s_game_icon);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleLeaderboardFailedEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Leaderboard {} ({}) failed", event->leaderboard->id, event->leaderboard->title);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
std::string title = event->leaderboard->title;
|
|
|
|
std::string message = TRANSLATE_STR("Achievements", "Leaderboard attempt failed.");
|
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
|
|
|
LEADERBOARD_FAILED_NOTIFICATION_TIME, std::move(title), std::move(message),
|
|
|
|
s_game_icon);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleLeaderboardSubmittedEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Leaderboard {} ({}) submitted", event->leaderboard->id, event->leaderboard->title);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = {
|
2023-09-18 12:29:47 +00:00
|
|
|
TRANSLATE_NOOP("Achievements", "Your Time: {}{}"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Your Score: {}{}"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Your Value: {}{}"),
|
2023-09-07 10:13:48 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
std::string title = event->leaderboard->title;
|
2023-09-18 12:29:47 +00:00
|
|
|
std::string message = fmt::format(
|
|
|
|
fmt::runtime(Host::TranslateToStringView(
|
|
|
|
"Achievements",
|
|
|
|
value_strings[std::min<u8>(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])),
|
|
|
|
event->leaderboard->tracker_value ? event->leaderboard->tracker_value : "Unknown",
|
|
|
|
g_settings.achievements_spectator_mode ? std::string_view() : TRANSLATE_SV("Achievements", " (Submitting)"));
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
2023-09-18 12:29:47 +00:00
|
|
|
static_cast<float>(g_settings.achievements_leaderboard_duration), std::move(title),
|
|
|
|
std::move(message), s_game_icon);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (g_settings.achievements_sound_effects)
|
2024-01-10 03:35:06 +00:00
|
|
|
PlatformMisc::PlaySoundAsync(EmuFolders::GetOverridableResourcePath(LBSUBMIT_SOUND_NAME).c_str());
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Leaderboard {} scoreboard rank {} of {}", event->leaderboard_scoreboard->leaderboard_id,
|
|
|
|
event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (g_settings.achievements_leaderboard_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = {
|
|
|
|
TRANSLATE_NOOP("Achievements", "Your Time: {} (Best: {})"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Your Score: {} (Best: {})"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Your Value: {} (Best: {})"),
|
|
|
|
};
|
|
|
|
|
|
|
|
std::string title = event->leaderboard->title;
|
|
|
|
std::string message = fmt::format(
|
|
|
|
TRANSLATE_FS("Achievements", "{}\nLeaderboard Position: {} of {}"),
|
|
|
|
fmt::format(fmt::runtime(Host::TranslateToStringView(
|
|
|
|
"Achievements",
|
|
|
|
value_strings[std::min<u8>(event->leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)])),
|
|
|
|
event->leaderboard_scoreboard->submitted_score, event->leaderboard_scoreboard->best_score),
|
|
|
|
event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries);
|
|
|
|
|
|
|
|
ImGuiFullscreen::AddNotification(fmt::format("leaderboard_{}", event->leaderboard->id),
|
2023-09-18 12:29:47 +00:00
|
|
|
static_cast<float>(g_settings.achievements_leaderboard_duration), std::move(title),
|
|
|
|
std::move(message), s_game_icon);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleLeaderboardTrackerShowEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Showing leaderboard tracker: {}: {}", event->leaderboard_tracker->id, event->leaderboard_tracker->display);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
TinyString width_string;
|
2023-09-20 13:49:14 +00:00
|
|
|
width_string.append(ICON_FA_STOPWATCH);
|
2023-09-07 10:13:48 +00:00
|
|
|
const u32 display_len = static_cast<u32>(std::strlen(event->leaderboard_tracker->display));
|
|
|
|
for (u32 i = 0; i < display_len; i++)
|
2023-09-20 13:49:14 +00:00
|
|
|
width_string.append('0');
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
LeaderboardTrackerIndicator indicator;
|
|
|
|
indicator.tracker_id = event->leaderboard_tracker->id;
|
2023-11-30 04:10:28 +00:00
|
|
|
indicator.text = event->leaderboard_tracker->display;
|
2023-09-07 10:13:48 +00:00
|
|
|
indicator.active = true;
|
|
|
|
s_active_leaderboard_trackers.push_back(std::move(indicator));
|
2022-09-21 12:02:54 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
const u32 id = event->leaderboard_tracker->id;
|
|
|
|
auto it = std::find_if(s_active_leaderboard_trackers.begin(), s_active_leaderboard_trackers.end(),
|
|
|
|
[id](const auto& it) { return it.tracker_id == id; });
|
|
|
|
if (it == s_active_leaderboard_trackers.end())
|
|
|
|
return;
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Hiding leaderboard tracker: {}", id);
|
2023-09-07 10:13:48 +00:00
|
|
|
it->active = false;
|
|
|
|
it->show_hide_time.Reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
const u32 id = event->leaderboard_tracker->id;
|
|
|
|
auto it = std::find_if(s_active_leaderboard_trackers.begin(), s_active_leaderboard_trackers.end(),
|
|
|
|
[id](const auto& it) { return it.tracker_id == id; });
|
|
|
|
if (it == s_active_leaderboard_trackers.end())
|
|
|
|
return;
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Updating leaderboard tracker: {}: {}", event->leaderboard_tracker->id, event->leaderboard_tracker->display);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
2023-11-30 04:10:28 +00:00
|
|
|
it->text = event->leaderboard_tracker->display;
|
2023-09-07 10:13:48 +00:00
|
|
|
it->active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
if (auto it =
|
|
|
|
std::find_if(s_active_challenge_indicators.begin(), s_active_challenge_indicators.end(),
|
|
|
|
[event](const AchievementChallengeIndicator& it) { return it.achievement == event->achievement; });
|
|
|
|
it != s_active_challenge_indicators.end())
|
|
|
|
{
|
|
|
|
it->show_hide_time.Reset();
|
|
|
|
it->active = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
AchievementChallengeIndicator indicator;
|
|
|
|
indicator.achievement = event->achievement;
|
|
|
|
indicator.badge_path = GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED);
|
|
|
|
indicator.active = true;
|
|
|
|
s_active_challenge_indicators.push_back(std::move(indicator));
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Show challenge indicator for {} ({})", event->achievement->id, event->achievement->title);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
auto it =
|
|
|
|
std::find_if(s_active_challenge_indicators.begin(), s_active_challenge_indicators.end(),
|
|
|
|
[event](const AchievementChallengeIndicator& it) { return it.achievement == event->achievement; });
|
|
|
|
if (it == s_active_challenge_indicators.end())
|
|
|
|
return;
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Hide challenge indicator for {} ({})", event->achievement->id, event->achievement->title);
|
2023-09-07 10:13:48 +00:00
|
|
|
it->show_hide_time.Reset();
|
|
|
|
it->active = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleAchievementProgressIndicatorShowEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Showing progress indicator: {} ({}): {}", event->achievement->id, event->achievement->title,
|
|
|
|
event->achievement->measured_progress);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (!s_active_progress_indicator.has_value())
|
|
|
|
s_active_progress_indicator.emplace();
|
|
|
|
else
|
|
|
|
s_active_progress_indicator->show_hide_time.Reset();
|
|
|
|
|
|
|
|
s_active_progress_indicator->achievement = event->achievement;
|
|
|
|
s_active_progress_indicator->badge_path =
|
|
|
|
GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED);
|
|
|
|
s_active_progress_indicator->active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
if (!s_active_progress_indicator.has_value())
|
|
|
|
return;
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Hiding progress indicator");
|
2023-09-07 10:13:48 +00:00
|
|
|
s_active_progress_indicator->show_hide_time.Reset();
|
|
|
|
s_active_progress_indicator->active = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleAchievementProgressIndicatorUpdateEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Updating progress indicator: {} ({}): {}", event->achievement->id, event->achievement->title,
|
|
|
|
event->achievement->measured_progress);
|
2023-09-07 10:13:48 +00:00
|
|
|
s_active_progress_indicator->achievement = event->achievement;
|
|
|
|
s_active_progress_indicator->active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleServerErrorEvent(const rc_client_event_t* event)
|
|
|
|
{
|
|
|
|
std::string message =
|
|
|
|
fmt::format(TRANSLATE_FS("Achievements", "Server error in {}:\n{}"),
|
|
|
|
event->server_error->api ? event->server_error->api : "UNKNOWN",
|
|
|
|
event->server_error->error_message ? event->server_error->error_message : "UNKNOWN");
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG(message.c_str());
|
2023-09-07 10:13:48 +00:00
|
|
|
Host::AddOSDMessage(std::move(message), Host::OSD_ERROR_DURATION);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleServerDisconnectedEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
WARNING_LOG("Server disconnected.");
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
ImGuiFullscreen::ShowToast(
|
|
|
|
TRANSLATE_STR("Achievements", "Achievements Disconnected"),
|
|
|
|
TRANSLATE_STR("Achievements",
|
|
|
|
"An unlock request could not be completed. We will keep retrying to submit this request."),
|
|
|
|
Host::OSD_ERROR_DURATION);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::HandleServerReconnectedEvent(const rc_client_event_t* event)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
WARNING_LOG("Server reconnected.");
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (FullscreenUI::Initialize())
|
|
|
|
{
|
|
|
|
ImGuiFullscreen::ShowToast(TRANSLATE_STR("Achievements", "Achievements Reconnected"),
|
|
|
|
TRANSLATE_STR("Achievements", "All pending unlock requests have completed."),
|
|
|
|
Host::OSD_INFO_DURATION);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::ResetClient()
|
2022-09-21 12:02:54 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-09-21 12:02:54 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
2022-07-11 13:03:29 +00:00
|
|
|
RA_OnReset();
|
2022-09-21 12:02:54 +00:00
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!IsActive())
|
2022-09-21 12:02:54 +00:00
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Reset client");
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_reset(s_client);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 12:02:54 +00:00
|
|
|
void Achievements::OnSystemPaused(bool paused)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
RA_SetPaused(paused);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DisableHardcoreMode()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
if (!IsActive())
|
|
|
|
return;
|
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
if (RA_HardcoreModeIsActive())
|
|
|
|
RA_DisableHardcore();
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2023-09-23 02:55:05 +00:00
|
|
|
if (!s_hardcore_mode)
|
|
|
|
return;
|
|
|
|
|
|
|
|
SetHardcoreMode(false, true);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2024-04-17 14:40:06 +00:00
|
|
|
bool Achievements::ResetHardcoreMode(bool is_booting)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!IsActive())
|
|
|
|
return false;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
|
|
|
|
|
|
|
// If we're not logged in, don't apply hardcore mode restrictions.
|
|
|
|
// If we later log in, we'll start with it off anyway.
|
2023-09-23 02:55:05 +00:00
|
|
|
const bool wanted_hardcore_mode =
|
|
|
|
(IsLoggedInOrLoggingIn() || s_load_game_request) && g_settings.achievements_hardcore_mode;
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_hardcore_mode == wanted_hardcore_mode)
|
|
|
|
return false;
|
|
|
|
|
2024-04-17 14:40:06 +00:00
|
|
|
if (!is_booting && wanted_hardcore_mode && !CanEnableHardcoreMode())
|
|
|
|
return false;
|
|
|
|
|
2023-09-23 02:55:05 +00:00
|
|
|
SetHardcoreMode(wanted_hardcore_mode, false);
|
2023-09-07 10:13:48 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-09-23 02:55:05 +00:00
|
|
|
void Achievements::SetHardcoreMode(bool enabled, bool force_display_message)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
if (enabled == s_hardcore_mode)
|
2023-08-23 12:06:48 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// new mode
|
|
|
|
s_hardcore_mode = enabled;
|
2023-08-23 12:06:48 +00:00
|
|
|
|
2023-09-23 02:55:05 +00:00
|
|
|
if (System::IsValid() && (HasActiveGame() || force_display_message) && FullscreenUI::Initialize())
|
2023-08-23 12:06:48 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::ShowToast(std::string(),
|
|
|
|
enabled ? TRANSLATE_STR("Achievements", "Hardcore mode is now enabled.") :
|
|
|
|
TRANSLATE_STR("Achievements", "Hardcore mode is now disabled."),
|
|
|
|
Host::OSD_INFO_DURATION);
|
2023-08-23 12:06:48 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
rc_client_set_hardcore_enabled(s_client, enabled);
|
|
|
|
DebugAssert((rc_client_get_hardcore_enabled(s_client) != 0) == enabled);
|
|
|
|
if (HasActiveGame())
|
2023-09-23 02:55:05 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
UpdateGameSummary();
|
2023-09-23 02:55:05 +00:00
|
|
|
DisplayAchievementSummary();
|
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
|
2023-12-16 11:12:05 +00:00
|
|
|
// Reload setting to permit cheating-like things if we were just disabled.
|
|
|
|
if (!enabled)
|
|
|
|
Host::RunOnCPUThread([]() { System::ApplySettings(false); });
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// Toss away UI state, because it's invalid now
|
|
|
|
ClearUIState();
|
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
Host::OnAchievementsHardcoreModeChanged(enabled);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool Achievements::DoState(StateWrapper& sw)
|
|
|
|
{
|
|
|
|
// if we're inactive, we still need to skip the data (if any)
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!IsActive())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
u32 data_size = 0;
|
|
|
|
sw.Do(&data_size);
|
|
|
|
if (data_size > 0)
|
|
|
|
sw.SkipBytes(data_size);
|
|
|
|
|
|
|
|
return !sw.HasError();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_lock lock(s_achievements_mutex);
|
|
|
|
|
|
|
|
if (sw.IsReading())
|
|
|
|
{
|
2022-09-18 03:24:37 +00:00
|
|
|
// if we're active, make sure we've downloaded and activated all the achievements
|
|
|
|
// before deserializing, otherwise that state's going to get lost.
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!IsUsingRAIntegration() && s_load_game_request)
|
2022-09-18 03:24:37 +00:00
|
|
|
{
|
|
|
|
Host::DisplayLoadingScreen("Downloading achievements data...");
|
|
|
|
s_http_downloader->WaitForAllRequests();
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
u32 data_size = 0;
|
|
|
|
sw.Do(&data_size);
|
|
|
|
if (data_size == 0)
|
|
|
|
{
|
|
|
|
// reset runtime, no data (state might've been created without cheevos)
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("State is missing cheevos data, resetting runtime");
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
RA_OnReset();
|
|
|
|
else
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_reset(s_client);
|
2022-07-11 13:03:29 +00:00
|
|
|
#else
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_reset(s_client);
|
2022-07-11 13:03:29 +00:00
|
|
|
#endif
|
|
|
|
|
|
|
|
return !sw.HasError();
|
|
|
|
}
|
|
|
|
|
2024-06-28 03:12:24 +00:00
|
|
|
if (data_size > s_state_buffer.size())
|
|
|
|
s_state_buffer.resize(data_size);
|
2024-06-24 01:56:46 +00:00
|
|
|
if (data_size > 0)
|
|
|
|
sw.DoBytes(s_state_buffer.data(), data_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
if (sw.HasError())
|
|
|
|
return false;
|
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
2024-06-24 01:56:46 +00:00
|
|
|
RA_RestoreState(reinterpret_cast<const char*>(s_state_buffer.data()));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-06-24 01:56:46 +00:00
|
|
|
const int result = rc_client_deserialize_progress_sized(s_client, s_state_buffer.data(), data_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
if (result != RC_OK)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
WARNING_LOG("Failed to deserialize cheevos state ({}), resetting", result);
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_reset(s_client);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2024-06-28 03:12:24 +00:00
|
|
|
size_t data_size;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
|
|
|
const int size = RA_CaptureState(nullptr, 0);
|
|
|
|
|
|
|
|
data_size = (size >= 0) ? static_cast<u32>(size) : 0;
|
2024-06-24 01:56:46 +00:00
|
|
|
s_state_buffer.resize(data_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-06-24 01:56:46 +00:00
|
|
|
if (data_size > 0)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-06-24 01:56:46 +00:00
|
|
|
const int result = RA_CaptureState(reinterpret_cast<char*>(s_state_buffer.data()), static_cast<int>(data_size));
|
|
|
|
if (result != static_cast<int>(data_size))
|
|
|
|
{
|
|
|
|
WARNING_LOG("Failed to serialize cheevos state from RAIntegration.");
|
|
|
|
data_size = 0;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
2022-08-10 03:04:20 +00:00
|
|
|
#endif
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-06-28 03:12:24 +00:00
|
|
|
data_size = rc_client_progress_size(s_client);
|
|
|
|
if (data_size > 0)
|
2024-06-24 01:56:46 +00:00
|
|
|
{
|
2024-06-28 03:12:24 +00:00
|
|
|
if (s_state_buffer.size() < data_size)
|
|
|
|
s_state_buffer.resize(data_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-06-28 03:12:24 +00:00
|
|
|
const int result = rc_client_serialize_progress_sized(s_client, s_state_buffer.data(), data_size);
|
|
|
|
if (result != RC_OK)
|
|
|
|
{
|
|
|
|
// set data to zero, effectively serializing nothing
|
|
|
|
WARNING_LOG("Failed to serialize cheevos state ({})", result);
|
|
|
|
data_size = 0;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sw.Do(&data_size);
|
|
|
|
if (data_size > 0)
|
2024-06-24 01:56:46 +00:00
|
|
|
sw.DoBytes(s_state_buffer.data(), data_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
return !sw.HasError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
std::string Achievements::GetAchievementBadgePath(const rc_client_achievement_t* achievement, int state,
|
|
|
|
bool download_if_missing)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-08-04 07:00:06 +00:00
|
|
|
const std::string path = GetLocalImagePath(achievement->badge_name, (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ?
|
|
|
|
RC_IMAGE_TYPE_ACHIEVEMENT :
|
|
|
|
RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED);
|
|
|
|
if (download_if_missing && !path.empty() && !FileSystem::FileExists(path.c_str()))
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
char buf[512];
|
|
|
|
const int res = rc_client_achievement_get_image_url(achievement, state, buf, std::size(buf));
|
|
|
|
if (res == RC_OK)
|
|
|
|
DownloadImage(buf, path);
|
|
|
|
else
|
|
|
|
ReportRCError(res, "rc_client_achievement_get_image_url() for {} failed", achievement->title);
|
|
|
|
}
|
|
|
|
|
|
|
|
return path;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
std::string Achievements::GetLeaderboardUserBadgePath(const rc_client_leaderboard_entry_t* entry)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
// TODO: maybe we should just cache these in memory...
|
2024-08-04 07:00:06 +00:00
|
|
|
const std::string path = GetLocalImagePath(entry->user, RC_IMAGE_TYPE_USER);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (!FileSystem::FileExists(path.c_str()))
|
|
|
|
{
|
|
|
|
char buf[512];
|
|
|
|
const int res = rc_client_leaderboard_entry_get_user_image_url(entry, buf, std::size(buf));
|
|
|
|
if (res == RC_OK)
|
|
|
|
DownloadImage(buf, path);
|
|
|
|
else
|
|
|
|
ReportRCError(res, "rc_client_leaderboard_entry_get_user_image_url() for {} failed", entry->user);
|
|
|
|
}
|
|
|
|
|
|
|
|
return path;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-23 02:55:05 +00:00
|
|
|
bool Achievements::IsLoggedInOrLoggingIn()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-23 02:55:05 +00:00
|
|
|
return (rc_client_get_user_info(s_client) != nullptr || s_login_request);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-04-17 14:40:06 +00:00
|
|
|
bool Achievements::CanEnableHardcoreMode()
|
|
|
|
{
|
|
|
|
return (s_load_game_request || s_has_achievements || s_has_leaderboards);
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::Login(const char* username, const char* password, Error* error)
|
|
|
|
{
|
|
|
|
auto lock = GetLock();
|
|
|
|
|
|
|
|
// We need to use a temporary client if achievements aren't currently active.
|
|
|
|
rc_client_t* client = s_client;
|
2023-11-06 09:59:02 +00:00
|
|
|
HTTPDownloader* http = s_http_downloader.get();
|
2023-09-07 10:13:48 +00:00
|
|
|
const bool is_temporary_client = (client == nullptr);
|
2023-11-06 09:59:02 +00:00
|
|
|
std::unique_ptr<HTTPDownloader> temporary_downloader;
|
2023-09-07 10:13:48 +00:00
|
|
|
ScopedGuard temporary_client_guard = [&client, is_temporary_client, &temporary_downloader]() {
|
|
|
|
if (is_temporary_client)
|
|
|
|
DestroyClient(&client, &temporary_downloader);
|
|
|
|
};
|
|
|
|
if (is_temporary_client)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!CreateClient(&client, &temporary_downloader))
|
|
|
|
{
|
|
|
|
Error::SetString(error, "Failed to create client.");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
http = temporary_downloader.get();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
LoginWithPasswordParameters params = {username, error, nullptr, false};
|
|
|
|
|
|
|
|
params.request =
|
|
|
|
rc_client_begin_login_with_password(client, username, password, ClientLoginWithPasswordCallback, ¶ms);
|
|
|
|
if (!params.request)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
Error::SetString(error, "Failed to create login request.");
|
|
|
|
return false;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
// Wait until the login request completes.
|
|
|
|
http->WaitForAllRequests();
|
|
|
|
Assert(!params.request);
|
|
|
|
|
|
|
|
// Success? Assume the callback set the error message.
|
|
|
|
if (!params.result)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// If we were't a temporary client, get the game loaded.
|
|
|
|
if (System::IsValid() && !is_temporary_client)
|
|
|
|
BeginLoadGame();
|
|
|
|
|
|
|
|
return true;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ClientLoginWithPasswordCallback(int result, const char* error_message, rc_client_t* client,
|
|
|
|
void* userdata)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
Assert(userdata);
|
|
|
|
|
|
|
|
LoginWithPasswordParameters* params = static_cast<LoginWithPasswordParameters*>(userdata);
|
|
|
|
params->request = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (result != RC_OK)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("Login failed: {}: {}", rc_error_str(result), error_message ? error_message : "Unknown");
|
2023-09-07 10:13:48 +00:00
|
|
|
Error::SetString(params->error,
|
|
|
|
fmt::format("{}: {}", rc_error_str(result), error_message ? error_message : "Unknown"));
|
|
|
|
params->result = false;
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// Grab the token from the client, and save it to the config.
|
|
|
|
const rc_client_user_t* user = rc_client_get_user_info(client);
|
|
|
|
if (!user || !user->token)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("rc_client_get_user_info() returned NULL");
|
2023-09-07 10:13:48 +00:00
|
|
|
Error::SetString(params->error, "rc_client_get_user_info() returned NULL");
|
|
|
|
params->result = false;
|
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
params->result = true;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// Store configuration.
|
|
|
|
Host::SetBaseStringSettingValue("Cheevos", "Username", params->username);
|
|
|
|
Host::SetBaseStringSettingValue("Cheevos", "Token", user->token);
|
|
|
|
Host::SetBaseStringSettingValue("Cheevos", "LoginTimestamp", fmt::format("{}", std::time(nullptr)).c_str());
|
|
|
|
Host::CommitBaseSettingChanges();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ShowLoginSuccess(client);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ClientLoginWithTokenCallback(int result, const char* error_message, rc_client_t* client,
|
|
|
|
void* userdata)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
s_login_request = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (result != RC_OK)
|
|
|
|
{
|
|
|
|
ReportFmtError("Login failed: {}", error_message);
|
|
|
|
Host::OnAchievementsLoginRequested(LoginRequestReason::TokenInvalid);
|
|
|
|
return;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ShowLoginSuccess(client);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (System::IsValid())
|
|
|
|
BeginLoadGame();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ShowLoginSuccess(const rc_client_t* client)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const rc_client_user_t* user = rc_client_get_user_info(client);
|
|
|
|
if (!user)
|
|
|
|
return;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
Host::OnAchievementsLoginSuccess(user->username, user->score, user->score_softcore, user->num_unread_messages);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (System::IsValid())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
|
|
|
if (s_client == client)
|
|
|
|
Host::RunOnCPUThread(ShowLoginNotification);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ShowLoginNotification()
|
|
|
|
{
|
|
|
|
const rc_client_user_t* user = rc_client_get_user_info(s_client);
|
|
|
|
if (!user)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (g_settings.achievements_notifications && FullscreenUI::Initialize())
|
|
|
|
{
|
2024-04-09 10:04:45 +00:00
|
|
|
std::string badge_path = GetLoggedInUserBadgePath();
|
2024-04-10 10:28:00 +00:00
|
|
|
std::string title = user->display_name;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
//: Summary for login notification.
|
|
|
|
std::string summary = fmt::format(TRANSLATE_FS("Achievements", "Score: {} ({} softcore)\nUnread messages: {}"),
|
|
|
|
user->score, user->score_softcore, user->num_unread_messages);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::AddNotification("achievements_login", LOGIN_NOTIFICATION_TIME, std::move(title),
|
|
|
|
std::move(summary), std::move(badge_path));
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2024-04-09 10:04:45 +00:00
|
|
|
const char* Achievements::GetLoggedInUserName()
|
|
|
|
{
|
|
|
|
const rc_client_user_t* user = rc_client_get_user_info(s_client);
|
|
|
|
if (!user) [[unlikely]]
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
return user->username;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string Achievements::GetLoggedInUserBadgePath()
|
|
|
|
{
|
|
|
|
std::string badge_path;
|
|
|
|
|
|
|
|
const rc_client_user_t* user = rc_client_get_user_info(s_client);
|
|
|
|
if (!user) [[unlikely]]
|
|
|
|
return badge_path;
|
|
|
|
|
2024-08-04 07:00:06 +00:00
|
|
|
badge_path = GetLocalImagePath(user->username, RC_IMAGE_TYPE_USER);
|
|
|
|
if (!badge_path.empty() && !FileSystem::FileExists(badge_path.c_str())) [[unlikely]]
|
2024-04-09 10:04:45 +00:00
|
|
|
{
|
|
|
|
char url[512];
|
|
|
|
const int res = rc_client_user_get_image_url(user, url, std::size(url));
|
|
|
|
if (res == RC_OK)
|
|
|
|
DownloadImage(url, badge_path);
|
|
|
|
else
|
|
|
|
ReportRCError(res, "rc_client_user_get_image_url() failed: ");
|
|
|
|
}
|
|
|
|
|
|
|
|
return badge_path;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
void Achievements::Logout()
|
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
if (IsActive())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
|
|
|
|
|
|
|
if (HasActiveGame())
|
2022-07-11 13:03:29 +00:00
|
|
|
ClearGameInfo();
|
2023-09-07 10:13:48 +00:00
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Logging out...");
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_logout(s_client);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Clearing credentials...");
|
2022-07-11 13:03:29 +00:00
|
|
|
Host::DeleteBaseSettingValue("Cheevos", "Username");
|
|
|
|
Host::DeleteBaseSettingValue("Cheevos", "Token");
|
|
|
|
Host::DeleteBaseSettingValue("Cheevos", "LoginTimestamp");
|
|
|
|
Host::CommitBaseSettingChanges();
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::ConfirmSystemReset()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2023-09-07 10:13:48 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
return RA_ConfirmLoadNewRom(false);
|
|
|
|
#endif
|
2022-09-21 12:54:37 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
return true;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::ConfirmHardcoreModeDisable(const char* trigger)
|
2022-09-24 03:27:59 +00:00
|
|
|
{
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2023-09-07 10:13:48 +00:00
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
return (RA_WarnDisableHardcore(trigger) != 0);
|
|
|
|
#endif
|
2022-09-24 03:27:59 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
// I really hope this doesn't deadlock :/
|
|
|
|
const bool confirmed = Host::ConfirmMessage(
|
|
|
|
TRANSLATE("Achievements", "Confirm Hardcore Mode"),
|
|
|
|
fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you "
|
|
|
|
"want to disable hardcore mode? {0} will be cancelled if you select No."),
|
|
|
|
trigger));
|
|
|
|
if (!confirmed)
|
|
|
|
return false;
|
2022-09-24 03:27:59 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
DisableHardcoreMode();
|
|
|
|
return true;
|
2022-09-24 03:27:59 +00:00
|
|
|
}
|
|
|
|
|
2023-11-26 12:11:18 +00:00
|
|
|
void Achievements::ConfirmHardcoreModeDisableAsync(const char* trigger, std::function<void(bool)> callback)
|
|
|
|
{
|
2023-12-15 13:54:25 +00:00
|
|
|
#ifndef __ANDROID__
|
2023-11-26 12:11:18 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
|
|
|
if (IsUsingRAIntegration())
|
|
|
|
{
|
|
|
|
const bool result = (RA_WarnDisableHardcore(trigger) != 0);
|
|
|
|
callback(result);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if (!FullscreenUI::Initialize())
|
|
|
|
{
|
2023-12-15 13:54:25 +00:00
|
|
|
Host::AddOSDMessage(fmt::format(TRANSLATE_FS("Achievements", "Cannot {} while hardcode mode is active."), trigger),
|
2023-11-26 12:11:18 +00:00
|
|
|
Host::OSD_WARNING_DURATION);
|
|
|
|
callback(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto real_callback = [callback = std::move(callback)](bool res) mutable {
|
|
|
|
// don't run the callback in the middle of rendering the UI
|
|
|
|
Host::RunOnCPUThread([callback = std::move(callback), res]() {
|
|
|
|
if (res)
|
|
|
|
DisableHardcoreMode();
|
|
|
|
callback(res);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
ImGuiFullscreen::OpenConfirmMessageDialog(
|
|
|
|
TRANSLATE_STR("Achievements", "Confirm Hardcore Mode"),
|
|
|
|
fmt::format(TRANSLATE_FS("Achievements", "{0} cannot be performed while hardcore mode is active. Do you "
|
|
|
|
"want to disable hardcore mode? {0} will be cancelled if you select No."),
|
|
|
|
trigger),
|
|
|
|
std::move(real_callback), fmt::format(ICON_FA_CHECK " {}", TRANSLATE_SV("Achievements", "Yes")),
|
|
|
|
fmt::format(ICON_FA_TIMES " {}", TRANSLATE_SV("Achievements", "No")));
|
2023-12-15 13:54:25 +00:00
|
|
|
#else
|
|
|
|
Host::AddOSDMessage(fmt::format(TRANSLATE_FS("Achievements", "Cannot {} while hardcode mode is active."), trigger),
|
|
|
|
Host::OSD_WARNING_DURATION);
|
|
|
|
callback(false);
|
|
|
|
#endif
|
2023-11-26 12:11:18 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::ClearUIState()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-10-31 15:32:29 +00:00
|
|
|
#ifndef __ANDROID__
|
2023-09-07 10:13:48 +00:00
|
|
|
if (FullscreenUI::IsAchievementsWindowOpen() || FullscreenUI::IsLeaderboardsWindowOpen())
|
2023-09-18 12:29:47 +00:00
|
|
|
FullscreenUI::ReturnToPreviousWindow();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
CloseLeaderboard();
|
|
|
|
#endif
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_achievement_badge_paths = {};
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_leaderboard_user_icon_paths = {};
|
|
|
|
s_leaderboard_entry_lists = {};
|
|
|
|
if (s_leaderboard_list)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_destroy_leaderboard_list(s_leaderboard_list);
|
|
|
|
s_leaderboard_list = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_achievement_list)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_destroy_achievement_list(s_achievement_list);
|
|
|
|
s_achievement_list = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
template<typename T>
|
|
|
|
static float IndicatorOpacity(const T& i)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const float elapsed = static_cast<float>(i.show_hide_time.GetTimeSeconds());
|
|
|
|
const float time = i.active ? Achievements::INDICATOR_FADE_IN_TIME : Achievements::INDICATOR_FADE_OUT_TIME;
|
|
|
|
const float opacity = (elapsed >= time) ? 1.0f : (elapsed / time);
|
|
|
|
return (i.active) ? opacity : (1.0f - opacity);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DrawGameOverlays()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
using ImGuiFullscreen::g_medium_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!HasActiveGame() || !g_settings.achievements_overlays)
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float spacing = LayoutScale(10.0f);
|
|
|
|
const float padding = LayoutScale(10.0f);
|
|
|
|
const ImVec2 image_size =
|
|
|
|
LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT);
|
|
|
|
const ImGuiIO& io = ImGui::GetIO();
|
|
|
|
ImVec2 position = ImVec2(io.DisplaySize.x - padding, io.DisplaySize.y - padding);
|
|
|
|
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!s_active_challenge_indicators.empty())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const float x_advance = image_size.x + spacing;
|
|
|
|
ImVec2 current_position = ImVec2(position.x - image_size.x, position.y - image_size.y);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
for (auto it = s_active_challenge_indicators.begin(); it != s_active_challenge_indicators.end();)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const AchievementChallengeIndicator& indicator = *it;
|
|
|
|
const float opacity = IndicatorOpacity(indicator);
|
|
|
|
const u32 col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity));
|
|
|
|
|
|
|
|
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path);
|
|
|
|
if (badge)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
dl->AddImage(badge, current_position, current_position + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
|
|
|
col);
|
|
|
|
current_position.x -= x_advance;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!indicator.active && opacity <= 0.01f)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Remove challenge indicator");
|
2023-09-07 10:13:48 +00:00
|
|
|
it = s_active_challenge_indicators.erase(it);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
++it;
|
|
|
|
}
|
2022-10-08 10:25:34 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
position.y -= image_size.y + padding;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_active_progress_indicator.has_value())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const AchievementProgressIndicator& indicator = s_active_progress_indicator.value();
|
|
|
|
const float opacity = IndicatorOpacity(indicator);
|
|
|
|
const u32 col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity));
|
|
|
|
|
|
|
|
const char* text_start = s_active_progress_indicator->achievement->measured_progress;
|
|
|
|
const char* text_end = text_start + std::strlen(text_start);
|
|
|
|
const ImVec2 text_size = g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, text_start, text_end);
|
|
|
|
|
|
|
|
const ImVec2 box_min = ImVec2(position.x - image_size.x - text_size.x - spacing - padding * 2.0f,
|
|
|
|
position.y - image_size.y - padding * 2.0f);
|
|
|
|
const ImVec2 box_max = position;
|
|
|
|
const float box_rounding = LayoutScale(1.0f);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ImVec4(0.13f, 0.13f, 0.13f, opacity * 0.5f)), box_rounding);
|
|
|
|
dl->AddRect(box_min, box_max, ImGui::GetColorU32(ImVec4(0.8f, 0.8f, 0.8f, opacity)), box_rounding);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path);
|
|
|
|
if (badge)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImVec2 badge_pos = box_min + ImVec2(padding, padding);
|
|
|
|
dl->AddImage(badge, badge_pos, badge_pos + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), col);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
const ImVec2 text_pos =
|
|
|
|
box_min + ImVec2(padding + image_size.x + spacing, (box_max.y - box_min.y - text_size.y) * 0.5f);
|
|
|
|
const ImVec4 text_clip_rect(text_pos.x, text_pos.y, box_max.x, box_max.y);
|
|
|
|
dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, col, text_start, text_end, 0.0f, &text_clip_rect);
|
|
|
|
|
|
|
|
if (!indicator.active && opacity <= 0.01f)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Remove progress indicator");
|
2023-09-07 10:13:48 +00:00
|
|
|
s_active_progress_indicator.reset();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-11-30 04:10:28 +00:00
|
|
|
|
|
|
|
position.y -= image_size.y - padding * 3.0f;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!s_active_leaderboard_trackers.empty())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
for (auto it = s_active_leaderboard_trackers.begin(); it != s_active_leaderboard_trackers.end();)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const LeaderboardTrackerIndicator& indicator = *it;
|
|
|
|
const float opacity = IndicatorOpacity(indicator);
|
|
|
|
|
2023-11-30 04:10:28 +00:00
|
|
|
TinyString width_string;
|
|
|
|
width_string.append(ICON_FA_STOPWATCH);
|
2023-12-04 04:12:32 +00:00
|
|
|
for (u32 i = 0; i < indicator.text.length(); i++)
|
2023-11-30 04:10:28 +00:00
|
|
|
width_string.append('0');
|
|
|
|
const ImVec2 size = ImGuiFullscreen::g_medium_font->CalcTextSizeA(
|
|
|
|
ImGuiFullscreen::g_medium_font->FontSize, FLT_MAX, 0.0f, width_string.c_str(), width_string.end_ptr());
|
|
|
|
|
|
|
|
const ImVec2 box_min = ImVec2(position.x - size.x - padding * 2.0f, position.y - size.y - padding * 2.0f);
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImVec2 box_max = position;
|
|
|
|
const float box_rounding = LayoutScale(1.0f);
|
|
|
|
dl->AddRectFilled(box_min, box_max, ImGui::GetColorU32(ImVec4(0.13f, 0.13f, 0.13f, opacity * 0.5f)),
|
|
|
|
box_rounding);
|
|
|
|
dl->AddRect(box_min, box_max, ImGui::GetColorU32(ImVec4(0.8f, 0.8f, 0.8f, opacity)), box_rounding);
|
|
|
|
|
|
|
|
const u32 text_col = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, opacity));
|
2023-11-30 04:10:28 +00:00
|
|
|
const ImVec2 text_size = ImGuiFullscreen::g_medium_font->CalcTextSizeA(
|
|
|
|
ImGuiFullscreen::g_medium_font->FontSize, FLT_MAX, 0.0f, indicator.text.c_str(),
|
|
|
|
indicator.text.c_str() + indicator.text.length());
|
|
|
|
const ImVec2 text_pos = ImVec2(box_max.x - padding - text_size.x, box_min.y + padding);
|
|
|
|
const ImVec4 text_clip_rect(box_min.x, box_min.y, box_max.x, box_max.y);
|
2023-09-07 10:13:48 +00:00
|
|
|
dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos, text_col, indicator.text.c_str(),
|
|
|
|
indicator.text.c_str() + indicator.text.length(), 0.0f, &text_clip_rect);
|
|
|
|
|
2023-11-30 04:10:28 +00:00
|
|
|
const ImVec2 icon_pos = ImVec2(box_min.x + padding, box_min.y + padding);
|
|
|
|
dl->AddText(g_medium_font, g_medium_font->FontSize, icon_pos, text_col, ICON_FA_STOPWATCH, nullptr, 0.0f,
|
|
|
|
&text_clip_rect);
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!indicator.active && opacity <= 0.01f)
|
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Remove tracker indicator");
|
2023-09-07 10:13:48 +00:00
|
|
|
it = s_active_leaderboard_trackers.erase(it);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
++it;
|
|
|
|
}
|
2023-11-30 04:10:28 +00:00
|
|
|
|
|
|
|
position.x = box_min.x - padding;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-11-30 04:10:28 +00:00
|
|
|
// Uncomment if there are any other overlays above this one.
|
|
|
|
// position.y -= image_size.y - padding * 3.0f;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
#ifndef __ANDROID__
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DrawPauseMenuOverlays()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
using ImGuiFullscreen::g_large_font;
|
|
|
|
using ImGuiFullscreen::g_medium_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!HasActiveGame())
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_active_challenge_indicators.empty() && !s_active_progress_indicator.has_value())
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImGuiIO& io = ImGui::GetIO();
|
|
|
|
ImFont* font = g_medium_font;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImVec2 image_size(LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY,
|
|
|
|
ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY));
|
|
|
|
const float start_y = LayoutScale(10.0f + 4.0f + 4.0f) + g_large_font->FontSize + (g_medium_font->FontSize * 2.0f);
|
|
|
|
const float margin = LayoutScale(10.0f);
|
|
|
|
const float spacing = LayoutScale(10.0f);
|
|
|
|
const float padding = LayoutScale(10.0f);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float max_text_width = ImGuiFullscreen::LayoutScale(300.0f);
|
|
|
|
const float row_width = max_text_width + padding + padding + image_size.x + spacing;
|
|
|
|
const float title_height = padding + font->FontSize + padding;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!s_active_challenge_indicators.empty())
|
|
|
|
{
|
|
|
|
const ImVec2 box_min(io.DisplaySize.x - row_width - margin, start_y + margin);
|
|
|
|
const ImVec2 box_max(box_min.x + row_width,
|
|
|
|
box_min.y + title_height +
|
|
|
|
(static_cast<float>(s_active_challenge_indicators.size()) * (image_size.y + padding)));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
|
|
|
dl->AddRectFilled(box_min, box_max, IM_COL32(0x21, 0x21, 0x21, 200), LayoutScale(10.0f));
|
|
|
|
dl->AddText(font, font->FontSize, ImVec2(box_min.x + padding, box_min.y + padding), IM_COL32(255, 255, 255, 255),
|
|
|
|
TRANSLATE("Achievements", "Active Challenge Achievements"));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float y_advance = image_size.y + spacing;
|
|
|
|
const float acheivement_name_offset = (image_size.y - font->FontSize) / 2.0f;
|
|
|
|
const float max_non_ellipised_text_width = max_text_width - LayoutScale(10.0f);
|
|
|
|
ImVec2 position(box_min.x + padding, box_min.y + title_height);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
for (const AchievementChallengeIndicator& indicator : s_active_challenge_indicators)
|
|
|
|
{
|
|
|
|
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path);
|
|
|
|
if (!badge)
|
|
|
|
continue;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
dl->AddImage(badge, position, position + image_size);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const char* achievement_title = indicator.achievement->title;
|
|
|
|
const char* achievement_title_end = achievement_title + std::strlen(indicator.achievement->title);
|
|
|
|
const char* remaining_text = nullptr;
|
|
|
|
const ImVec2 text_width(font->CalcTextSizeA(font->FontSize, max_non_ellipised_text_width, 0.0f, achievement_title,
|
|
|
|
achievement_title_end, &remaining_text));
|
|
|
|
const ImVec2 text_position(position.x + image_size.x + spacing, position.y + acheivement_name_offset);
|
|
|
|
const ImVec4 text_bbox(text_position.x, text_position.y, text_position.x + max_text_width,
|
|
|
|
text_position.y + image_size.y);
|
|
|
|
const u32 text_color = IM_COL32(255, 255, 255, 255);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (remaining_text < achievement_title_end)
|
|
|
|
{
|
|
|
|
dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, remaining_text, 0.0f,
|
|
|
|
&text_bbox);
|
|
|
|
dl->AddText(font, font->FontSize, ImVec2(text_position.x + text_width.x, text_position.y), text_color, "...",
|
|
|
|
nullptr, 0.0f, &text_bbox);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
dl->AddText(font, font->FontSize, text_position, text_color, achievement_title, achievement_title_end, 0.0f,
|
|
|
|
&text_bbox);
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
position.y += y_advance;
|
|
|
|
}
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::PrepareAchievementsWindow()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = Achievements::GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_achievement_badge_paths = {};
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_achievement_list)
|
|
|
|
rc_client_destroy_achievement_list(s_achievement_list);
|
|
|
|
s_achievement_list = rc_client_create_achievement_list(
|
|
|
|
s_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
|
|
|
|
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS /*RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE*/);
|
|
|
|
if (!s_achievement_list)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("rc_client_create_achievement_list() returned null");
|
2023-09-07 10:13:48 +00:00
|
|
|
return false;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
return true;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DrawAchievementsWindow()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
using ImGuiFullscreen::g_large_font;
|
|
|
|
using ImGuiFullscreen::g_medium_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
|
|
|
|
|
|
|
if (!s_achievement_list)
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = Achievements::GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static constexpr float alpha = 0.8f;
|
|
|
|
static constexpr float heading_alpha = 0.95f;
|
|
|
|
static constexpr float heading_height_unscaled = 110.0f;
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-19 14:09:31 +00:00
|
|
|
const ImVec4 background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, alpha);
|
|
|
|
const ImVec4 heading_background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, heading_alpha);
|
|
|
|
const ImVec2 display_size = ImGui::GetIO().DisplaySize;
|
2023-09-07 10:13:48 +00:00
|
|
|
const float heading_height = ImGuiFullscreen::LayoutScale(heading_height_unscaled);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (ImGuiFullscreen::BeginFullscreenWindow(
|
2024-05-12 08:14:51 +00:00
|
|
|
ImVec2(), ImVec2(display_size.x, heading_height), "achievements_heading", heading_background, 0.0f, ImVec2(),
|
|
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse))
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
ImRect bb;
|
|
|
|
bool visible, hovered;
|
|
|
|
ImGuiFullscreen::MenuButtonFrame("achievements_heading", false, heading_height_unscaled, &visible, &hovered,
|
|
|
|
&bb.Min, &bb.Max, 0, heading_alpha);
|
|
|
|
if (visible)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const float padding = ImGuiFullscreen::LayoutScale(10.0f);
|
|
|
|
const float spacing = ImGuiFullscreen::LayoutScale(10.0f);
|
|
|
|
const float image_height = ImGuiFullscreen::LayoutScale(85.0f);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImVec2 icon_min(bb.Min + ImVec2(padding, padding));
|
|
|
|
const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!s_game_icon.empty())
|
|
|
|
{
|
|
|
|
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(s_game_icon.c_str());
|
|
|
|
if (badge)
|
|
|
|
{
|
|
|
|
ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
|
|
|
IM_COL32(255, 255, 255, 255));
|
|
|
|
}
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
float left = bb.Min.x + padding + image_height + spacing;
|
|
|
|
float right = bb.Max.x - padding;
|
|
|
|
float top = bb.Min.y + padding;
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
SmallString text;
|
|
|
|
ImVec2 text_size;
|
|
|
|
|
|
|
|
if (ImGuiFullscreen::FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true,
|
|
|
|
g_large_font) ||
|
|
|
|
ImGuiFullscreen::WantsToCloseMenu())
|
|
|
|
{
|
2023-09-18 12:29:47 +00:00
|
|
|
FullscreenUI::ReturnToPreviousWindow();
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize));
|
2023-09-20 13:49:14 +00:00
|
|
|
text.assign(s_game_title);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (s_hardcore_mode)
|
2023-09-20 13:49:14 +00:00
|
|
|
text.append(TRANSLATE_SV("Achievements", " (Hardcore Mode)"));
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
top += g_large_font->FontSize + spacing;
|
|
|
|
|
|
|
|
ImGui::PushFont(g_large_font);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f),
|
|
|
|
&title_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
|
|
|
|
|
|
|
const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize));
|
2023-12-01 17:20:49 +00:00
|
|
|
if (s_game_summary.num_core_achievements > 0)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
2023-12-01 17:20:49 +00:00
|
|
|
if (s_game_summary.num_unlocked_achievements == s_game_summary.num_core_achievements)
|
|
|
|
{
|
2023-12-13 11:06:15 +00:00
|
|
|
text.format(TRANSLATE_FS("Achievements", "You have unlocked all achievements and earned {} points!"),
|
2023-12-15 13:54:25 +00:00
|
|
|
s_game_summary.points_unlocked);
|
2023-12-01 17:20:49 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-12-13 11:06:15 +00:00
|
|
|
text.format(TRANSLATE_FS("Achievements",
|
2023-12-15 13:54:25 +00:00
|
|
|
"You have unlocked {0} of {1} achievements, earning {2} of {3} possible points."),
|
|
|
|
s_game_summary.num_unlocked_achievements, s_game_summary.num_core_achievements,
|
|
|
|
s_game_summary.points_unlocked, s_game_summary.points_core);
|
2023-12-01 17:20:49 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2023-12-01 17:20:49 +00:00
|
|
|
text.assign(TRANSLATE_SV("Achievements", "This game has no achievements."));
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
top += g_medium_font->FontSize + spacing;
|
|
|
|
|
|
|
|
ImGui::PushFont(g_medium_font);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.c_str(), text.end_ptr(), nullptr,
|
|
|
|
ImVec2(0.0f, 0.0f), &summary_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
|
|
|
|
2023-12-01 17:20:49 +00:00
|
|
|
if (s_game_summary.num_core_achievements > 0)
|
|
|
|
{
|
|
|
|
const float progress_height = ImGuiFullscreen::LayoutScale(20.0f);
|
|
|
|
const ImRect progress_bb(ImVec2(left, top), ImVec2(right, top + progress_height));
|
|
|
|
const float fraction = static_cast<float>(s_game_summary.num_unlocked_achievements) /
|
|
|
|
static_cast<float>(s_game_summary.num_core_achievements);
|
|
|
|
dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor));
|
|
|
|
dl->AddRectFilled(progress_bb.Min,
|
|
|
|
ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y),
|
|
|
|
ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor));
|
|
|
|
|
2023-12-13 11:06:15 +00:00
|
|
|
text.format("{}%", static_cast<int>(std::round(fraction * 100.0f)));
|
2023-12-01 17:20:49 +00:00
|
|
|
text_size = ImGui::CalcTextSize(text.c_str(), text.end_ptr());
|
|
|
|
const ImVec2 text_pos(
|
|
|
|
progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f),
|
|
|
|
progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f));
|
|
|
|
dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos,
|
|
|
|
ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), text.c_str(), text.end_ptr());
|
|
|
|
top += progress_height + spacing;
|
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::EndFullscreenWindow();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::SetNextWindowBgAlpha(alpha);
|
|
|
|
|
2024-04-18 10:09:56 +00:00
|
|
|
if (ImGuiFullscreen::BeginFullscreenWindow(
|
|
|
|
ImVec2(0.0f, heading_height),
|
|
|
|
ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)),
|
2024-05-12 08:14:51 +00:00
|
|
|
"achievements", background, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0))
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
static bool buckets_collapsed[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS] = {};
|
2023-09-18 12:29:47 +00:00
|
|
|
static const char* bucket_names[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS] = {
|
|
|
|
TRANSLATE_NOOP("Achievements", "Unknown"), TRANSLATE_NOOP("Achievements", "Locked"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Unlocked"), TRANSLATE_NOOP("Achievements", "Unsupported"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Unofficial"), TRANSLATE_NOOP("Achievements", "Recently Unlocked"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Active Challenges"), TRANSLATE_NOOP("Achievements", "Almost There"),
|
|
|
|
};
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
ImGuiFullscreen::BeginMenuButtons();
|
|
|
|
|
|
|
|
for (u32 bucket_type : {RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE,
|
|
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED,
|
|
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED,
|
|
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED})
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
for (u32 bucket_idx = 0; bucket_idx < s_achievement_list->num_buckets; bucket_idx++)
|
|
|
|
{
|
|
|
|
const rc_client_achievement_bucket_t& bucket = s_achievement_list->buckets[bucket_idx];
|
|
|
|
if (bucket.bucket_type != bucket_type)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
DebugAssert(bucket.bucket_type < NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS);
|
|
|
|
|
2023-09-18 12:29:47 +00:00
|
|
|
// TODO: Once subsets are supported, this will need to change.
|
2023-09-07 10:13:48 +00:00
|
|
|
bool& bucket_collapsed = buckets_collapsed[bucket.bucket_type];
|
2023-09-18 12:29:47 +00:00
|
|
|
bucket_collapsed ^=
|
|
|
|
ImGuiFullscreen::MenuHeadingButton(Host::TranslateToCString("Achievements", bucket_names[bucket.bucket_type]),
|
|
|
|
bucket_collapsed ? ICON_FA_CHEVRON_DOWN : ICON_FA_CHEVRON_UP);
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!bucket_collapsed)
|
|
|
|
{
|
|
|
|
for (u32 i = 0; i < bucket.num_achievements; i++)
|
|
|
|
DrawAchievement(bucket.achievements[i]);
|
|
|
|
}
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::EndMenuButtons();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::EndFullscreenWindow();
|
2024-04-09 10:04:45 +00:00
|
|
|
|
|
|
|
FullscreenUI::SetStandardSelectionFooterText(true);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
using ImGuiFullscreen::g_large_font;
|
|
|
|
using ImGuiFullscreen::g_medium_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
2024-01-13 04:44:06 +00:00
|
|
|
using ImGuiFullscreen::LayoutUnscale;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static constexpr float alpha = 0.8f;
|
|
|
|
static constexpr float progress_height_unscaled = 20.0f;
|
|
|
|
static constexpr float progress_spacing_unscaled = 5.0f;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float spacing = ImGuiFullscreen::LayoutScale(4.0f);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const bool is_unlocked = (cheevo->state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED);
|
|
|
|
const std::string_view measured_progress(cheevo->measured_progress);
|
|
|
|
const bool is_measured = !is_unlocked && !measured_progress.empty();
|
|
|
|
const float unlock_size = is_unlocked ? (spacing + ImGuiFullscreen::LAYOUT_MEDIUM_FONT_SIZE) : 0.0f;
|
2024-01-13 04:44:06 +00:00
|
|
|
const ImVec2 points_template_size(
|
|
|
|
g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, TRANSLATE("Achievements", "XXX points")));
|
|
|
|
|
|
|
|
const size_t summary_length = std::strlen(cheevo->description);
|
|
|
|
const float summary_wrap_width =
|
|
|
|
(ImGui::GetCurrentWindow()->WorkRect.GetWidth() - (ImGui::GetStyle().FramePadding.x * 2.0f) -
|
|
|
|
LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + 30.0f) - points_template_size.x);
|
|
|
|
const ImVec2 summary_text_size(g_medium_font->CalcTextSizeA(
|
|
|
|
g_medium_font->FontSize, FLT_MAX, summary_wrap_width, cheevo->description, cheevo->description + summary_length));
|
|
|
|
|
|
|
|
// Messy, but need to undo LayoutScale in MenuButtonFrame()...
|
|
|
|
const float extra_summary_height = LayoutUnscale(std::max(summary_text_size.y - g_medium_font->FontSize, 0.0f));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImRect bb;
|
|
|
|
bool visible, hovered;
|
2024-01-13 04:39:35 +00:00
|
|
|
const bool clicked = ImGuiFullscreen::MenuButtonFrame(
|
|
|
|
TinyString::from_format("chv_{}", cheevo->id), true,
|
2024-01-13 04:44:06 +00:00
|
|
|
!is_measured ? ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + extra_summary_height + unlock_size :
|
|
|
|
ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT + extra_summary_height + progress_height_unscaled +
|
|
|
|
progress_spacing_unscaled,
|
2024-01-13 04:39:35 +00:00
|
|
|
&visible, &hovered, &bb.Min, &bb.Max, 0, alpha);
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!visible)
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
std::string* badge_path;
|
|
|
|
if (const auto badge_it = std::find_if(s_achievement_badge_paths.begin(), s_achievement_badge_paths.end(),
|
|
|
|
[cheevo](const auto& it) { return (it.first == cheevo); });
|
|
|
|
badge_it != s_achievement_badge_paths.end())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
badge_path = &badge_it->second;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::string new_badge_path = Achievements::GetAchievementBadgePath(cheevo, cheevo->state);
|
|
|
|
badge_path = &s_achievement_badge_paths.emplace_back(cheevo, std::move(new_badge_path)).second;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImVec2 image_size(
|
|
|
|
LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT));
|
|
|
|
if (!badge_path->empty())
|
|
|
|
{
|
|
|
|
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(*badge_path);
|
|
|
|
if (badge)
|
|
|
|
{
|
|
|
|
ImGui::GetWindowDrawList()->AddImage(badge, bb.Min, bb.Min + image_size, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
|
|
|
IM_COL32(255, 255, 255, 255));
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
SmallString text;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float midpoint = bb.Min.y + g_large_font->FontSize + spacing;
|
2023-12-15 13:54:25 +00:00
|
|
|
text.format((cheevo->points != 1) ? TRANSLATE_FS("Achievements", "{} points") :
|
|
|
|
TRANSLATE_FS("Achievements", "{} point"),
|
|
|
|
cheevo->points);
|
2023-09-20 13:49:14 +00:00
|
|
|
const ImVec2 points_size(
|
|
|
|
g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, text.c_str(), text.end_ptr()));
|
2023-09-07 10:13:48 +00:00
|
|
|
const float points_template_start = bb.Max.x - points_template_size.x;
|
|
|
|
const float points_start = points_template_start + ((points_template_size.x - points_size.x) * 0.5f);
|
2024-01-13 04:34:57 +00:00
|
|
|
|
|
|
|
const char* right_icon_text;
|
|
|
|
switch (cheevo->type)
|
|
|
|
{
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_TYPE_MISSABLE:
|
|
|
|
right_icon_text = ICON_PF_ACHIEVEMENTS_MISSABLE; // Missable
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_TYPE_PROGRESSION:
|
|
|
|
right_icon_text = ICON_PF_ACHIEVEMENTS_PROGRESSION; // Progression
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_TYPE_WIN:
|
|
|
|
right_icon_text = ICON_PF_ACHIEVEMENTS_WIN; // Win Condition
|
|
|
|
break;
|
|
|
|
|
|
|
|
// Just use the lock for standard achievements.
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_TYPE_STANDARD:
|
|
|
|
default:
|
|
|
|
right_icon_text = is_unlocked ? ICON_FA_LOCK_OPEN : ICON_FA_LOCK;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ImVec2 right_icon_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, FLT_MAX, 0.0f, right_icon_text));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float text_start_x = bb.Min.x + image_size.x + LayoutScale(15.0f);
|
|
|
|
const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(points_start, midpoint));
|
2024-01-13 04:44:06 +00:00
|
|
|
const ImRect summary_bb(ImVec2(text_start_x, midpoint),
|
|
|
|
ImVec2(points_start, midpoint + g_medium_font->FontSize + extra_summary_height));
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect points_bb(ImVec2(points_start, midpoint), bb.Max);
|
2024-01-13 04:34:57 +00:00
|
|
|
const ImRect lock_bb(ImVec2(points_template_start + ((points_template_size.x - right_icon_size.x) * 0.5f), bb.Min.y),
|
2023-09-07 10:13:48 +00:00
|
|
|
ImVec2(bb.Max.x, midpoint));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_large_font);
|
|
|
|
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, cheevo->title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
|
2024-01-13 04:34:57 +00:00
|
|
|
ImGui::RenderTextClipped(lock_bb.Min, lock_bb.Max, right_icon_text, nullptr, &right_icon_size, ImVec2(0.0f, 0.0f),
|
|
|
|
&lock_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_medium_font);
|
2024-01-13 04:44:06 +00:00
|
|
|
if (cheevo->description && summary_length > 0)
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
2024-01-13 04:44:06 +00:00
|
|
|
ImGui::RenderTextWrapped(summary_bb.Min, cheevo->description, cheevo->description + summary_length,
|
|
|
|
summary_wrap_width);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(points_bb.Min, points_bb.Max, text.c_str(), text.end_ptr(), &points_size, ImVec2(0.0f, 0.0f),
|
|
|
|
&points_bb);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (is_unlocked)
|
|
|
|
{
|
|
|
|
TinyString date;
|
|
|
|
FullscreenUI::TimeToPrintableString(&date, cheevo->unlock_time);
|
2023-12-13 11:06:15 +00:00
|
|
|
text.format(TRANSLATE_FS("Achievements", "Unlocked: {}"), date);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect unlock_bb(summary_bb.Min.x, summary_bb.Max.y + spacing, summary_bb.Max.x, bb.Max.y);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(unlock_bb.Min, unlock_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f),
|
|
|
|
&unlock_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
else if (is_measured)
|
|
|
|
{
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
const float progress_height = LayoutScale(progress_height_unscaled);
|
|
|
|
const float progress_spacing = LayoutScale(progress_spacing_unscaled);
|
|
|
|
const float top = midpoint + g_medium_font->FontSize + progress_spacing;
|
|
|
|
const ImRect progress_bb(ImVec2(text_start_x, top), ImVec2(bb.Max.x, top + progress_height));
|
|
|
|
const float fraction = cheevo->measured_percent * 0.01f;
|
|
|
|
dl->AddRectFilled(progress_bb.Min, progress_bb.Max, ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryDarkColor));
|
|
|
|
dl->AddRectFilled(progress_bb.Min, ImVec2(progress_bb.Min.x + fraction * progress_bb.GetWidth(), progress_bb.Max.y),
|
|
|
|
ImGui::GetColorU32(ImGuiFullscreen::UISecondaryColor));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImVec2 text_size =
|
|
|
|
ImGui::CalcTextSize(measured_progress.data(), measured_progress.data() + measured_progress.size());
|
|
|
|
const ImVec2 text_pos(progress_bb.Min.x + ((progress_bb.Max.x - progress_bb.Min.x) / 2.0f) - (text_size.x / 2.0f),
|
|
|
|
progress_bb.Min.y + ((progress_bb.Max.y - progress_bb.Min.y) / 2.0f) - (text_size.y / 2.0f));
|
|
|
|
dl->AddText(g_medium_font, g_medium_font->FontSize, text_pos,
|
|
|
|
ImGui::GetColorU32(ImGuiFullscreen::UIPrimaryTextColor), measured_progress.data(),
|
|
|
|
measured_progress.data() + measured_progress.size());
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-01-13 04:39:35 +00:00
|
|
|
if (clicked)
|
|
|
|
{
|
|
|
|
const SmallString url = SmallString::from_format(fmt::runtime(ACHEIVEMENT_DETAILS_URL_TEMPLATE), cheevo->id);
|
2024-05-23 10:55:28 +00:00
|
|
|
INFO_LOG("Opening achievement details: {}", url);
|
2024-01-13 04:39:35 +00:00
|
|
|
Host::OpenURL(url);
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::PrepareLeaderboardsWindow()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = Achievements::GetLock();
|
|
|
|
rc_client_t* const client = s_client;
|
|
|
|
|
|
|
|
s_achievement_badge_paths = {};
|
|
|
|
CloseLeaderboard();
|
|
|
|
if (s_leaderboard_list)
|
|
|
|
rc_client_destroy_leaderboard_list(s_leaderboard_list);
|
|
|
|
s_leaderboard_list = rc_client_create_leaderboard_list(client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE);
|
|
|
|
if (!s_leaderboard_list)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
ERROR_LOG("rc_client_create_leaderboard_list() returned null");
|
2023-09-07 10:13:48 +00:00
|
|
|
return false;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DrawLeaderboardsWindow()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
using ImGuiFullscreen::g_large_font;
|
|
|
|
using ImGuiFullscreen::g_medium_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static constexpr float alpha = 0.8f;
|
|
|
|
static constexpr float heading_alpha = 0.95f;
|
|
|
|
static constexpr float heading_height_unscaled = 110.0f;
|
|
|
|
static constexpr float tab_height_unscaled = 50.0f;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
auto lock = Achievements::GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const bool is_leaderboard_open = (s_open_leaderboard != nullptr);
|
|
|
|
bool close_leaderboard_on_exit = false;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImRect bb;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-19 14:09:31 +00:00
|
|
|
const ImVec4 background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, alpha);
|
|
|
|
const ImVec4 heading_background = ImGuiFullscreen::ModAlpha(ImGuiFullscreen::UIBackgroundColor, heading_alpha);
|
|
|
|
const ImVec2 display_size = ImGui::GetIO().DisplaySize;
|
2023-09-07 10:13:48 +00:00
|
|
|
const float padding = LayoutScale(10.0f);
|
|
|
|
const float spacing = LayoutScale(10.0f);
|
|
|
|
const float spacing_small = spacing / 2.0f;
|
|
|
|
float heading_height = LayoutScale(heading_height_unscaled);
|
|
|
|
if (is_leaderboard_open)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
// tabs
|
|
|
|
heading_height += spacing_small + LayoutScale(tab_height_unscaled) + spacing;
|
|
|
|
|
|
|
|
// Add space for a legend - spacing + 1 line of text + spacing + line
|
|
|
|
heading_height += LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float rank_column_width =
|
|
|
|
g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "99999").x;
|
|
|
|
const float name_column_width =
|
|
|
|
g_large_font
|
|
|
|
->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "WWWWWWWWWWWWWWWWWWWWWW")
|
|
|
|
.x;
|
|
|
|
const float time_column_width =
|
|
|
|
g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, "WWWWWWWWWWW").x;
|
|
|
|
const float column_spacing = spacing * 2.0f;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (ImGuiFullscreen::BeginFullscreenWindow(
|
2024-05-12 08:14:51 +00:00
|
|
|
ImVec2(), ImVec2(display_size.x, heading_height), "leaderboards_heading", heading_background, 0.0f, ImVec2(),
|
|
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoScrollWithMouse))
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
bool visible, hovered;
|
|
|
|
bool pressed = ImGuiFullscreen::MenuButtonFrame("leaderboards_heading", false, heading_height_unscaled, &visible,
|
|
|
|
&hovered, &bb.Min, &bb.Max, 0, alpha);
|
|
|
|
UNREFERENCED_VARIABLE(pressed);
|
|
|
|
|
|
|
|
if (visible)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const float image_height = LayoutScale(85.0f);
|
|
|
|
|
|
|
|
const ImVec2 icon_min(bb.Min + ImVec2(padding, padding));
|
|
|
|
const ImVec2 icon_max(icon_min + ImVec2(image_height, image_height));
|
|
|
|
|
|
|
|
if (!s_game_icon.empty())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(s_game_icon.c_str());
|
|
|
|
if (badge)
|
|
|
|
{
|
|
|
|
ImGui::GetWindowDrawList()->AddImage(badge, icon_min, icon_max, ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f),
|
|
|
|
IM_COL32(255, 255, 255, 255));
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
float left = bb.Min.x + padding + image_height + spacing;
|
|
|
|
float right = bb.Max.x - padding;
|
|
|
|
float top = bb.Min.y + padding;
|
|
|
|
SmallString text;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!is_leaderboard_open)
|
|
|
|
{
|
|
|
|
if (ImGuiFullscreen::FloatingButton(ICON_FA_WINDOW_CLOSE, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true,
|
|
|
|
g_large_font) ||
|
|
|
|
ImGuiFullscreen::WantsToCloseMenu())
|
|
|
|
{
|
2023-09-18 12:29:47 +00:00
|
|
|
FullscreenUI::ReturnToPreviousWindow();
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (ImGuiFullscreen::FloatingButton(ICON_FA_CARET_SQUARE_LEFT, 10.0f, 10.0f, -1.0f, -1.0f, 1.0f, 0.0f, true,
|
|
|
|
g_large_font) ||
|
|
|
|
ImGuiFullscreen::WantsToCloseMenu())
|
|
|
|
{
|
|
|
|
close_leaderboard_on_exit = true;
|
|
|
|
}
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect title_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize));
|
2023-09-20 13:49:14 +00:00
|
|
|
text.assign(Achievements::GetGameTitle());
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
top += g_large_font->FontSize + spacing;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_large_font);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f),
|
|
|
|
&title_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (is_leaderboard_open)
|
|
|
|
{
|
|
|
|
const ImRect subtitle_bb(ImVec2(left, top), ImVec2(right, top + g_large_font->FontSize));
|
2023-09-20 13:49:14 +00:00
|
|
|
text.assign(s_open_leaderboard->title);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
top += g_large_font->FontSize + spacing_small;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_large_font);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(subtitle_bb.Min, subtitle_bb.Max, text.c_str(), text.end_ptr(), nullptr,
|
|
|
|
ImVec2(0.0f, 0.0f), &subtitle_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
2022-09-24 03:27:59 +00:00
|
|
|
|
2023-09-20 13:49:14 +00:00
|
|
|
text.assign(s_open_leaderboard->description);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
u32 count = 0;
|
|
|
|
for (u32 i = 0; i < s_leaderboard_list->num_buckets; i++)
|
|
|
|
count += s_leaderboard_list->buckets[i].num_leaderboards;
|
2023-12-13 11:06:15 +00:00
|
|
|
text.format(TRANSLATE_FS("Achievements", "This game has {} leaderboards."), count);
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
2022-09-24 03:27:59 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect summary_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize));
|
|
|
|
top += g_medium_font->FontSize + spacing_small;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_medium_font);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, text.c_str(), text.end_ptr(), nullptr,
|
|
|
|
ImVec2(0.0f, 0.0f), &summary_bb);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!is_leaderboard_open && !Achievements::IsHardcoreModeActive())
|
|
|
|
{
|
|
|
|
const ImRect hardcore_warning_bb(ImVec2(left, top), ImVec2(right, top + g_medium_font->FontSize));
|
|
|
|
top += g_medium_font->FontSize + spacing_small;
|
|
|
|
|
|
|
|
ImGui::RenderTextClipped(
|
|
|
|
hardcore_warning_bb.Min, hardcore_warning_bb.Max,
|
|
|
|
TRANSLATE("Achievements",
|
|
|
|
"Submitting scores is disabled because hardcore mode is off. Leaderboards are read-only."),
|
|
|
|
nullptr, nullptr, ImVec2(0.0f, 0.0f), &hardcore_warning_bb);
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (is_leaderboard_open)
|
|
|
|
{
|
|
|
|
const float tab_width = (ImGui::GetWindowWidth() / ImGuiFullscreen::g_layout_scale) * 0.5f;
|
|
|
|
ImGui::SetCursorPos(ImVec2(0.0f, top + spacing_small));
|
|
|
|
|
2024-01-14 10:24:00 +00:00
|
|
|
if (ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakSlow, false) ||
|
|
|
|
ImGui::IsKeyPressed(ImGuiKey_NavGamepadTweakFast, false))
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
s_is_showing_all_leaderboard_entries = !s_is_showing_all_leaderboard_entries;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const bool show_all : {false, true})
|
|
|
|
{
|
|
|
|
const char* title =
|
|
|
|
show_all ? TRANSLATE("Achievements", "Show Best") : TRANSLATE("Achievements", "Show Nearby");
|
|
|
|
if (ImGuiFullscreen::NavTab(title, s_is_showing_all_leaderboard_entries == show_all, true, tab_width,
|
|
|
|
tab_height_unscaled, heading_background))
|
|
|
|
{
|
|
|
|
s_is_showing_all_leaderboard_entries = show_all;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const ImVec2 bg_pos =
|
|
|
|
ImVec2(0.0f, ImGui::GetCurrentWindow()->DC.CursorPos.y + LayoutScale(tab_height_unscaled));
|
|
|
|
const ImVec2 bg_size =
|
|
|
|
ImVec2(ImGui::GetWindowWidth(),
|
|
|
|
spacing + LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + spacing);
|
|
|
|
ImGui::GetWindowDrawList()->AddRectFilled(bg_pos, bg_pos + bg_size, ImGui::GetColorU32(heading_background));
|
|
|
|
|
|
|
|
ImGui::SetCursorPos(ImVec2(0.0f, ImGui::GetCursorPosY() + LayoutScale(tab_height_unscaled) + spacing));
|
|
|
|
|
|
|
|
pressed =
|
|
|
|
ImGuiFullscreen::MenuButtonFrame("legend", false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY,
|
|
|
|
&visible, &hovered, &bb.Min, &bb.Max, 0, alpha);
|
|
|
|
UNREFERENCED_VARIABLE(pressed);
|
|
|
|
|
|
|
|
const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
|
|
|
|
float text_start_x = bb.Min.x + LayoutScale(15.0f) + padding;
|
|
|
|
|
|
|
|
ImGui::PushFont(g_large_font);
|
|
|
|
|
|
|
|
const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, TRANSLATE("Achievements", "Rank"), nullptr, nullptr,
|
|
|
|
ImVec2(0.0f, 0.0f), &rank_bb);
|
|
|
|
text_start_x += rank_column_width + column_spacing;
|
|
|
|
|
|
|
|
const ImRect user_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, TRANSLATE("Achievements", "Name"), nullptr, nullptr,
|
|
|
|
ImVec2(0.0f, 0.0f), &user_bb);
|
|
|
|
text_start_x += name_column_width + column_spacing;
|
|
|
|
|
|
|
|
static const char* value_headings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = {
|
|
|
|
TRANSLATE_NOOP("Achievements", "Time"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Score"),
|
|
|
|
TRANSLATE_NOOP("Achievements", "Value"),
|
|
|
|
};
|
|
|
|
|
|
|
|
const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
ImGui::RenderTextClipped(
|
|
|
|
score_bb.Min, score_bb.Max,
|
|
|
|
Host::TranslateToCString(
|
|
|
|
"Achievements",
|
|
|
|
value_headings[std::min<u8>(s_open_leaderboard->format, NUM_RC_CLIENT_LEADERBOARD_FORMATS - 1)]),
|
|
|
|
nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb);
|
|
|
|
text_start_x += time_column_width + column_spacing;
|
|
|
|
|
|
|
|
const ImRect date_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
ImGui::RenderTextClipped(date_bb.Min, date_bb.Max, TRANSLATE("Achievements", "Date Submitted"), nullptr,
|
|
|
|
nullptr, ImVec2(0.0f, 0.0f), &date_bb);
|
|
|
|
|
|
|
|
ImGui::PopFont();
|
|
|
|
|
|
|
|
const float line_thickness = LayoutScale(1.0f);
|
|
|
|
const float line_padding = LayoutScale(5.0f);
|
|
|
|
const ImVec2 line_start(bb.Min.x, bb.Min.y + g_large_font->FontSize + line_padding);
|
|
|
|
const ImVec2 line_end(bb.Max.x, line_start.y);
|
|
|
|
ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetColorU32(ImGuiCol_TextDisabled),
|
|
|
|
line_thickness);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ImGuiFullscreen::EndFullscreenWindow();
|
2024-04-09 10:04:45 +00:00
|
|
|
FullscreenUI::SetStandardSelectionFooterText(true);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!is_leaderboard_open)
|
|
|
|
{
|
2024-04-18 10:09:56 +00:00
|
|
|
if (ImGuiFullscreen::BeginFullscreenWindow(
|
|
|
|
ImVec2(0.0f, heading_height),
|
|
|
|
ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)),
|
2024-05-12 08:14:51 +00:00
|
|
|
"leaderboards", background, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0))
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
ImGuiFullscreen::BeginMenuButtons();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
for (u32 bucket_index = 0; bucket_index < s_leaderboard_list->num_buckets; bucket_index++)
|
|
|
|
{
|
|
|
|
const rc_client_leaderboard_bucket_t& bucket = s_leaderboard_list->buckets[bucket_index];
|
|
|
|
for (u32 i = 0; i < bucket.num_leaderboards; i++)
|
|
|
|
DrawLeaderboardListEntry(bucket.leaderboards[i]);
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::EndMenuButtons();
|
|
|
|
}
|
|
|
|
ImGuiFullscreen::EndFullscreenWindow();
|
2022-09-21 12:02:54 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
else
|
|
|
|
{
|
2024-04-18 10:09:56 +00:00
|
|
|
if (ImGuiFullscreen::BeginFullscreenWindow(
|
|
|
|
ImVec2(0.0f, heading_height),
|
|
|
|
ImVec2(display_size.x, display_size.y - heading_height - LayoutScale(ImGuiFullscreen::LAYOUT_FOOTER_HEIGHT)),
|
2024-05-12 08:14:51 +00:00
|
|
|
"leaderboard", background, 0.0f, ImVec2(ImGuiFullscreen::LAYOUT_MENU_WINDOW_X_PADDING, 0.0f), 0))
|
2023-09-07 10:13:48 +00:00
|
|
|
{
|
|
|
|
ImGuiFullscreen::BeginMenuButtons();
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!s_is_showing_all_leaderboard_entries)
|
|
|
|
{
|
|
|
|
if (s_leaderboard_nearby_entries)
|
|
|
|
{
|
|
|
|
for (u32 i = 0; i < s_leaderboard_nearby_entries->num_entries; i++)
|
|
|
|
{
|
|
|
|
DrawLeaderboardEntry(s_leaderboard_nearby_entries->entries[i],
|
|
|
|
static_cast<s32>(i) == s_leaderboard_nearby_entries->user_index, rank_column_width,
|
|
|
|
name_column_width, time_column_width, column_spacing);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
ImGui::PushFont(g_large_font);
|
|
|
|
|
|
|
|
const ImVec2 pos_min(0.0f, heading_height);
|
|
|
|
const ImVec2 pos_max(display_size.x, display_size.y);
|
|
|
|
ImGui::RenderTextClipped(pos_min, pos_max,
|
|
|
|
TRANSLATE("Achievements", "Downloading leaderboard data, please wait..."), nullptr,
|
|
|
|
nullptr, ImVec2(0.5f, 0.5f));
|
|
|
|
|
|
|
|
ImGui::PopFont();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for (const rc_client_leaderboard_entry_list_t* list : s_leaderboard_entry_lists)
|
|
|
|
{
|
|
|
|
for (u32 i = 0; i < list->num_entries; i++)
|
|
|
|
{
|
|
|
|
DrawLeaderboardEntry(list->entries[i], static_cast<s32>(i) == list->user_index, rank_column_width,
|
|
|
|
name_column_width, time_column_width, column_spacing);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch next chunk if the loading indicator becomes visible (i.e. we scrolled enough).
|
|
|
|
bool visible, hovered;
|
|
|
|
ImGuiFullscreen::MenuButtonFrame(TRANSLATE("Achievements", "Loading..."), false,
|
|
|
|
ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible, &hovered,
|
|
|
|
&bb.Min, &bb.Max);
|
|
|
|
if (visible)
|
|
|
|
{
|
|
|
|
const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
|
|
|
|
const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint));
|
|
|
|
|
|
|
|
ImGui::PushFont(g_large_font);
|
|
|
|
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, TRANSLATE("Achievements", "Loading..."), nullptr,
|
|
|
|
nullptr, ImVec2(0, 0), &title_bb);
|
|
|
|
ImGui::PopFont();
|
|
|
|
|
|
|
|
if (!s_leaderboard_fetch_handle)
|
|
|
|
FetchNextLeaderboardEntries();
|
|
|
|
}
|
|
|
|
}
|
2022-08-22 10:01:04 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::EndMenuButtons();
|
|
|
|
}
|
|
|
|
ImGuiFullscreen::EndFullscreenWindow();
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (close_leaderboard_on_exit)
|
|
|
|
CloseLeaderboard();
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::DrawLeaderboardEntry(const rc_client_leaderboard_entry_t& entry, bool is_self,
|
|
|
|
float rank_column_width, float name_column_width, float time_column_width,
|
|
|
|
float column_spacing)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
using ImGuiFullscreen::g_large_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static constexpr float alpha = 0.8f;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImRect bb;
|
|
|
|
bool visible, hovered;
|
|
|
|
bool pressed =
|
|
|
|
ImGuiFullscreen::MenuButtonFrame(entry.user, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, &visible,
|
|
|
|
&hovered, &bb.Min, &bb.Max, 0, alpha);
|
|
|
|
if (!visible)
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
|
|
|
|
float text_start_x = bb.Min.x + LayoutScale(15.0f);
|
|
|
|
SmallString text;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-12-13 11:06:15 +00:00
|
|
|
text.format("{}", entry.rank);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_large_font);
|
2022-09-21 12:54:37 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (is_self)
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(255, 242, 0, 255));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect rank_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(rank_bb.Min, rank_bb.Max, text.c_str(), text.end_ptr(), nullptr, ImVec2(0.0f, 0.0f),
|
|
|
|
&rank_bb);
|
2023-09-07 10:13:48 +00:00
|
|
|
text_start_x += rank_column_width + column_spacing;
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float icon_size = bb.Max.y - bb.Min.y;
|
|
|
|
const ImRect icon_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
GPUTexture* icon_tex = nullptr;
|
|
|
|
if (auto it = std::find_if(s_leaderboard_user_icon_paths.begin(), s_leaderboard_user_icon_paths.end(),
|
|
|
|
[&entry](const auto& it) { return it.first == &entry; });
|
|
|
|
it != s_leaderboard_user_icon_paths.end())
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
if (!it->second.empty())
|
|
|
|
icon_tex = ImGuiFullscreen::GetCachedTextureAsync(it->second);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
else
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
std::string path = Achievements::GetLeaderboardUserBadgePath(&entry);
|
|
|
|
if (!path.empty())
|
|
|
|
{
|
|
|
|
icon_tex = ImGuiFullscreen::GetCachedTextureAsync(path);
|
|
|
|
s_leaderboard_user_icon_paths.emplace_back(&entry, std::move(path));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (icon_tex)
|
|
|
|
{
|
|
|
|
ImGui::GetWindowDrawList()->AddImage(reinterpret_cast<ImTextureID>(icon_tex), icon_bb.Min,
|
|
|
|
icon_bb.Min + ImVec2(icon_size, icon_size));
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect user_bb(ImVec2(text_start_x + column_spacing + icon_size, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
ImGui::RenderTextClipped(user_bb.Min, user_bb.Max, entry.user, nullptr, nullptr, ImVec2(0.0f, 0.0f), &user_bb);
|
|
|
|
text_start_x += name_column_width + column_spacing;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect score_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
ImGui::RenderTextClipped(score_bb.Min, score_bb.Max, entry.display, nullptr, nullptr, ImVec2(0.0f, 0.0f), &score_bb);
|
|
|
|
text_start_x += time_column_width + column_spacing;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const ImRect time_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
SmallString submit_time;
|
|
|
|
FullscreenUI::TimeToPrintableString(&submit_time, entry.submitted);
|
2023-09-20 13:49:14 +00:00
|
|
|
ImGui::RenderTextClipped(time_bb.Min, time_bb.Max, submit_time.c_str(), submit_time.end_ptr(), nullptr,
|
|
|
|
ImVec2(0.0f, 0.0f), &time_bb);
|
2022-11-05 05:01:48 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (is_self)
|
|
|
|
ImGui::PopStyleColor();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PopFont();
|
2022-09-24 03:27:59 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (pressed)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
// Anything?
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
}
|
|
|
|
void Achievements::DrawLeaderboardListEntry(const rc_client_leaderboard_t* lboard)
|
|
|
|
{
|
|
|
|
using ImGuiFullscreen::g_large_font;
|
|
|
|
using ImGuiFullscreen::g_medium_font;
|
|
|
|
using ImGuiFullscreen::LayoutScale;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
static constexpr float alpha = 0.8f;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
TinyString id_str;
|
2023-12-13 11:06:15 +00:00
|
|
|
id_str.format("{}", lboard->id);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
ImRect bb;
|
|
|
|
bool visible, hovered;
|
|
|
|
bool pressed = ImGuiFullscreen::MenuButtonFrame(id_str, true, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, &visible,
|
|
|
|
&hovered, &bb.Min, &bb.Max, 0, alpha);
|
|
|
|
if (!visible)
|
2022-07-11 13:03:29 +00:00
|
|
|
return;
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
const float midpoint = bb.Min.y + g_large_font->FontSize + LayoutScale(4.0f);
|
|
|
|
const float text_start_x = bb.Min.x + LayoutScale(15.0f);
|
|
|
|
const ImRect title_bb(ImVec2(text_start_x, bb.Min.y), ImVec2(bb.Max.x, midpoint));
|
|
|
|
const ImRect summary_bb(ImVec2(text_start_x, midpoint), bb.Max);
|
|
|
|
|
|
|
|
ImGui::PushFont(g_large_font);
|
|
|
|
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, lboard->title, nullptr, nullptr, ImVec2(0.0f, 0.0f), &title_bb);
|
|
|
|
ImGui::PopFont();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (lboard->description && lboard->description[0] != '\0')
|
2022-09-21 13:45:24 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGui::PushFont(g_medium_font);
|
|
|
|
ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, lboard->description, nullptr, nullptr, ImVec2(0.0f, 0.0f),
|
|
|
|
&summary_bb);
|
|
|
|
ImGui::PopFont();
|
2022-09-21 13:45:24 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (pressed)
|
|
|
|
OpenLeaderboard(lboard);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
#endif // __ANDROID__
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::OpenLeaderboard(const rc_client_leaderboard_t* lboard)
|
2022-09-21 12:02:54 +00:00
|
|
|
{
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Opening leaderboard '{}' ({})", lboard->title, lboard->id);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
CloseLeaderboard();
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_open_leaderboard = lboard;
|
|
|
|
s_is_showing_all_leaderboard_entries = false;
|
|
|
|
s_leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries_around_user(
|
|
|
|
s_client, lboard->id, LEADERBOARD_NEARBY_ENTRIES_TO_FETCH, LeaderboardFetchNearbyCallback, nullptr);
|
2022-09-21 12:02:54 +00:00
|
|
|
}
|
|
|
|
|
2023-10-31 15:32:29 +00:00
|
|
|
bool Achievements::OpenLeaderboardById(u32 leaderboard_id)
|
|
|
|
{
|
|
|
|
const rc_client_leaderboard_t* lb = rc_client_get_leaderboard_info(s_client, leaderboard_id);
|
|
|
|
if (!lb)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
OpenLeaderboard(lb);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 Achievements::GetOpenLeaderboardId()
|
|
|
|
{
|
|
|
|
return s_open_leaderboard ? s_open_leaderboard->id : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Achievements::IsShowingAllLeaderboardEntries()
|
|
|
|
{
|
|
|
|
return s_is_showing_all_leaderboard_entries;
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::vector<rc_client_leaderboard_entry_list_t*>& Achievements::GetLeaderboardEntryLists()
|
|
|
|
{
|
|
|
|
return s_leaderboard_entry_lists;
|
|
|
|
}
|
|
|
|
|
|
|
|
const rc_client_leaderboard_entry_list_t* Achievements::GetLeaderboardNearbyEntries()
|
|
|
|
{
|
|
|
|
return s_leaderboard_nearby_entries;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::LeaderboardFetchNearbyCallback(int result, const char* error_message,
|
|
|
|
rc_client_leaderboard_entry_list_t* list, rc_client_t* client,
|
|
|
|
void* callback_userdata)
|
2022-09-21 12:02:54 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_leaderboard_fetch_handle = nullptr;
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (result != RC_OK)
|
|
|
|
{
|
|
|
|
ImGuiFullscreen::ShowToast(TRANSLATE("Achievements", "Leaderboard download failed"), error_message);
|
|
|
|
CloseLeaderboard();
|
|
|
|
return;
|
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_leaderboard_nearby_entries)
|
|
|
|
rc_client_destroy_leaderboard_entry_list(s_leaderboard_nearby_entries);
|
|
|
|
s_leaderboard_nearby_entries = list;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::LeaderboardFetchAllCallback(int result, const char* error_message,
|
|
|
|
rc_client_leaderboard_entry_list_t* list, rc_client_t* client,
|
|
|
|
void* callback_userdata)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
const auto lock = GetLock();
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_leaderboard_fetch_handle = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (result != RC_OK)
|
2022-08-10 03:04:20 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
ImGuiFullscreen::ShowToast(TRANSLATE("Achievements", "Leaderboard download failed"), error_message);
|
|
|
|
CloseLeaderboard();
|
|
|
|
return;
|
2022-08-10 03:04:20 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
s_leaderboard_entry_lists.push_back(list);
|
2022-08-10 03:04:20 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::FetchNextLeaderboardEntries()
|
2022-08-10 03:04:20 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
u32 start = 1;
|
|
|
|
for (rc_client_leaderboard_entry_list_t* list : s_leaderboard_entry_lists)
|
|
|
|
start += list->num_entries;
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2024-05-23 10:55:28 +00:00
|
|
|
DEV_LOG("Fetching entries {} to {}", start, start + LEADERBOARD_ALL_FETCH_SIZE);
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
if (s_leaderboard_fetch_handle)
|
|
|
|
rc_client_abort_async(s_client, s_leaderboard_fetch_handle);
|
|
|
|
s_leaderboard_fetch_handle = rc_client_begin_fetch_leaderboard_entries(
|
|
|
|
s_client, s_open_leaderboard->id, start, LEADERBOARD_ALL_FETCH_SIZE, LeaderboardFetchAllCallback, nullptr);
|
2022-09-21 12:02:54 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
void Achievements::CloseLeaderboard()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
s_leaderboard_user_icon_paths.clear();
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
for (auto iter = s_leaderboard_entry_lists.rbegin(); iter != s_leaderboard_entry_lists.rend(); ++iter)
|
|
|
|
rc_client_destroy_leaderboard_entry_list(*iter);
|
|
|
|
s_leaderboard_entry_lists.clear();
|
2022-09-21 12:02:54 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_leaderboard_nearby_entries)
|
|
|
|
{
|
|
|
|
rc_client_destroy_leaderboard_entry_list(s_leaderboard_nearby_entries);
|
|
|
|
s_leaderboard_nearby_entries = nullptr;
|
2022-09-21 12:02:54 +00:00
|
|
|
}
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
if (s_leaderboard_fetch_handle)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
rc_client_abort_async(s_client, s_leaderboard_fetch_handle);
|
|
|
|
s_leaderboard_fetch_handle = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
2023-09-07 10:13:48 +00:00
|
|
|
|
|
|
|
s_open_leaderboard = nullptr;
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
2023-09-17 02:28:11 +00:00
|
|
|
#ifdef ENABLE_RAINTEGRATION
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
#include "RA_Consoles.h"
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
bool Achievements::IsUsingRAIntegration()
|
|
|
|
{
|
|
|
|
return s_using_raintegration;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
namespace Achievements::RAIntegration {
|
|
|
|
static void InitializeRAIntegration(void* main_window_handle);
|
|
|
|
|
|
|
|
static int RACallbackIsActive();
|
|
|
|
static void RACallbackCauseUnpause();
|
|
|
|
static void RACallbackCausePause();
|
|
|
|
static void RACallbackRebuildMenu();
|
|
|
|
static void RACallbackEstimateTitle(char* buf);
|
|
|
|
static void RACallbackResetEmulator();
|
|
|
|
static void RACallbackLoadROM(const char* unused);
|
2024-06-24 02:20:01 +00:00
|
|
|
static unsigned char RACallbackReadRAM(unsigned int address);
|
|
|
|
static unsigned int RACallbackReadRAMBlock(unsigned int nAddress, unsigned char* pBuffer, unsigned int nBytes);
|
|
|
|
static void RACallbackWriteRAM(unsigned int address, unsigned char value);
|
|
|
|
static unsigned char RACallbackReadScratchpad(unsigned int address);
|
|
|
|
static unsigned int RACallbackReadScratchpadBlock(unsigned int nAddress, unsigned char* pBuffer, unsigned int nBytes);
|
|
|
|
static void RACallbackWriteScratchpad(unsigned int address, unsigned char value);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
static bool s_raintegration_initialized = false;
|
|
|
|
} // namespace Achievements::RAIntegration
|
|
|
|
|
|
|
|
void Achievements::SwitchToRAIntegration()
|
|
|
|
{
|
|
|
|
s_using_raintegration = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::InitializeRAIntegration(void* main_window_handle)
|
|
|
|
{
|
|
|
|
RA_InitClient((HWND)main_window_handle, "DuckStation", g_scm_tag_str);
|
2023-11-24 05:54:43 +00:00
|
|
|
RA_SetUserAgentDetail(Host::GetHTTPUserAgent().c_str());
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
RA_InstallSharedFunctions(RACallbackIsActive, RACallbackCauseUnpause, RACallbackCausePause, RACallbackRebuildMenu,
|
|
|
|
RACallbackEstimateTitle, RACallbackResetEmulator, RACallbackLoadROM);
|
|
|
|
RA_SetConsoleID(PlayStation);
|
|
|
|
|
|
|
|
// Apparently this has to be done early, or the memory inspector doesn't work.
|
|
|
|
// That's a bit unfortunate, because the RAM size can vary between games, and depending on the option.
|
2024-06-24 02:20:01 +00:00
|
|
|
RA_InstallMemoryBank(0, RACallbackReadRAM, RACallbackWriteRAM, Bus::RAM_2MB_SIZE);
|
|
|
|
RA_InstallMemoryBankBlockReader(0, RACallbackReadRAMBlock);
|
|
|
|
RA_InstallMemoryBank(1, RACallbackReadScratchpad, RACallbackWriteScratchpad, CPU::SCRATCHPAD_SIZE);
|
|
|
|
RA_InstallMemoryBankBlockReader(1, RACallbackReadScratchpadBlock);
|
2022-07-11 13:03:29 +00:00
|
|
|
|
|
|
|
// Fire off a login anyway. Saves going into the menu and doing it.
|
|
|
|
RA_AttemptLogin(0);
|
|
|
|
|
|
|
|
s_raintegration_initialized = true;
|
|
|
|
|
|
|
|
// this is pretty lame, but we may as well persist until we exit anyway
|
|
|
|
std::atexit(RA_Shutdown);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::MainWindowChanged(void* new_handle)
|
|
|
|
{
|
|
|
|
if (s_raintegration_initialized)
|
|
|
|
{
|
|
|
|
RA_UpdateHWnd((HWND)new_handle);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
InitializeRAIntegration(new_handle);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::GameChanged()
|
|
|
|
{
|
|
|
|
s_game_id = s_game_hash.empty() ? 0 : RA_IdentifyHash(s_game_hash.c_str());
|
|
|
|
RA_ActivateGame(s_game_id);
|
|
|
|
}
|
|
|
|
|
2022-08-22 10:01:04 +00:00
|
|
|
std::vector<std::tuple<int, std::string, bool>> Achievements::RAIntegration::GetMenuItems()
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2022-08-22 10:01:04 +00:00
|
|
|
std::array<RA_MenuItem, 64> items;
|
|
|
|
const int num_items = RA_GetPopupMenuItems(items.data());
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2022-08-22 10:01:04 +00:00
|
|
|
std::vector<std::tuple<int, std::string, bool>> ret;
|
|
|
|
ret.reserve(static_cast<u32>(num_items));
|
2022-07-11 13:03:29 +00:00
|
|
|
|
2022-08-22 10:01:04 +00:00
|
|
|
for (int i = 0; i < num_items; i++)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
2022-08-22 10:01:04 +00:00
|
|
|
const RA_MenuItem& it = items[i];
|
|
|
|
if (!it.sLabel)
|
|
|
|
ret.emplace_back(0, std::string(), false);
|
|
|
|
else
|
|
|
|
ret.emplace_back(static_cast<int>(it.nID), StringUtil::WideStringToUTF8String(it.sLabel), it.bChecked);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::ActivateMenuItem(int item)
|
|
|
|
{
|
|
|
|
RA_InvokeDialog(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
int Achievements::RAIntegration::RACallbackIsActive()
|
|
|
|
{
|
|
|
|
return static_cast<int>(HasActiveGame());
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackCauseUnpause()
|
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
Host::RunOnCPUThread([]() { System::PauseSystem(false); });
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackCausePause()
|
|
|
|
{
|
2023-09-07 10:13:48 +00:00
|
|
|
Host::RunOnCPUThread([]() { System::PauseSystem(true); });
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackRebuildMenu()
|
|
|
|
{
|
|
|
|
// unused, we build the menu on demand
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackEstimateTitle(char* buf)
|
|
|
|
{
|
2023-05-15 13:38:37 +00:00
|
|
|
StringUtil::Strlcpy(buf, System::GetGameTitle(), 256);
|
2022-07-11 13:03:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackResetEmulator()
|
|
|
|
{
|
|
|
|
if (System::IsValid())
|
|
|
|
System::ResetSystem();
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackLoadROM(const char* unused)
|
|
|
|
{
|
|
|
|
// unused
|
|
|
|
UNREFERENCED_PARAMETER(unused);
|
|
|
|
}
|
|
|
|
|
2024-06-24 02:20:01 +00:00
|
|
|
unsigned char Achievements::RAIntegration::RACallbackReadRAM(unsigned int address)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
if (!System::IsValid())
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
u8 value = 0;
|
|
|
|
CPU::SafeReadMemoryByte(address, &value);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2024-06-24 02:20:01 +00:00
|
|
|
void Achievements::RAIntegration::RACallbackWriteRAM(unsigned int address, unsigned char value)
|
2022-07-11 13:03:29 +00:00
|
|
|
{
|
|
|
|
CPU::SafeWriteMemoryByte(address, value);
|
|
|
|
}
|
|
|
|
|
2024-06-24 02:20:01 +00:00
|
|
|
unsigned int Achievements::RAIntegration::RACallbackReadRAMBlock(unsigned int nAddress, unsigned char* pBuffer,
|
|
|
|
unsigned int nBytes)
|
2022-08-22 10:01:04 +00:00
|
|
|
{
|
|
|
|
if (nAddress >= Bus::g_ram_size)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
const u32 copy_size = std::min<u32>(Bus::g_ram_size - nAddress, nBytes);
|
2023-10-07 08:10:09 +00:00
|
|
|
std::memcpy(pBuffer, Bus::g_unprotected_ram + nAddress, copy_size);
|
2022-08-22 10:01:04 +00:00
|
|
|
return copy_size;
|
|
|
|
}
|
|
|
|
|
2024-06-24 02:20:01 +00:00
|
|
|
unsigned char Achievements::RAIntegration::RACallbackReadScratchpad(unsigned int address)
|
|
|
|
{
|
|
|
|
if (!System::IsValid() || address >= CPU::SCRATCHPAD_SIZE)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
return CPU::g_state.scratchpad[address];
|
|
|
|
}
|
|
|
|
|
|
|
|
void Achievements::RAIntegration::RACallbackWriteScratchpad(unsigned int address, unsigned char value)
|
|
|
|
{
|
|
|
|
if (address >= CPU::SCRATCHPAD_SIZE)
|
|
|
|
return;
|
|
|
|
|
|
|
|
CPU::g_state.scratchpad[address] = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
unsigned int Achievements::RAIntegration::RACallbackReadScratchpadBlock(unsigned int nAddress, unsigned char* pBuffer,
|
|
|
|
unsigned int nBytes)
|
|
|
|
{
|
|
|
|
if (nAddress >= CPU::SCRATCHPAD_SIZE)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
const u32 copy_size = std::min<u32>(CPU::SCRATCHPAD_SIZE - nAddress, nBytes);
|
|
|
|
std::memcpy(pBuffer, &CPU::g_state.scratchpad[nAddress], copy_size);
|
|
|
|
return copy_size;
|
|
|
|
}
|
|
|
|
|
2023-09-07 10:13:48 +00:00
|
|
|
#else
|
|
|
|
|
|
|
|
bool Achievements::IsUsingRAIntegration()
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-07-11 13:03:29 +00:00
|
|
|
#endif
|