mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-18 12:24:36 +00:00
Achievements: Split up overlay setting
Split into challenge indicator modes, leaderboard trackers, and progress indicators.
This commit is contained in:
@@ -80,7 +80,7 @@
|
||||
<PropertyGroup>
|
||||
<MocDefines></MocDefines>
|
||||
<MocDefines Condition="!$(Configuration.Contains(Debug))">-DQT_NO_DEBUG -DNDEBUG $(MocDefines)</MocDefines>
|
||||
<MocIncludes>-I"$(QtIncludeDir)." -I"$(SolutionDir)pcsx2" "-I$(SolutionDir)." -I.</MocIncludes>
|
||||
<MocIncludes>-I"$(QtIncludeDir)." -I.</MocIncludes>
|
||||
</PropertyGroup>
|
||||
<Target Name="QtMoc"
|
||||
BeforeTargets="ClCompile"
|
||||
|
||||
@@ -112,6 +112,7 @@ struct AchievementChallengeIndicator
|
||||
{
|
||||
const rc_client_achievement_t* achievement;
|
||||
std::string badge_path;
|
||||
float time_remaining;
|
||||
float opacity;
|
||||
bool active;
|
||||
};
|
||||
@@ -729,6 +730,12 @@ void Achievements::UpdateSettings(const Settings& old_config)
|
||||
if (g_settings.achievements_unofficial_test_mode != old_config.achievements_unofficial_test_mode)
|
||||
rc_client_set_unofficial_enabled(s_state.client, g_settings.achievements_unofficial_test_mode);
|
||||
}
|
||||
|
||||
if (!g_settings.achievements_leaderboard_trackers)
|
||||
s_state.active_leaderboard_trackers.clear();
|
||||
|
||||
if (!g_settings.achievements_progress_indicators)
|
||||
s_state.active_progress_indicator.reset();
|
||||
}
|
||||
|
||||
void Achievements::Shutdown()
|
||||
@@ -1614,6 +1621,9 @@ void Achievements::HandleLeaderboardTrackerShowEvent(const rc_client_event_t* ev
|
||||
{
|
||||
DEV_LOG("Showing leaderboard tracker: {}: {}", event->leaderboard_tracker->id, event->leaderboard_tracker->display);
|
||||
|
||||
if (!g_settings.achievements_leaderboard_trackers)
|
||||
return;
|
||||
|
||||
const u32 id = event->leaderboard_tracker->id;
|
||||
auto it = std::find_if(s_state.active_leaderboard_trackers.begin(), s_state.active_leaderboard_trackers.end(),
|
||||
[id](const auto& it) { return it.tracker_id == id; });
|
||||
@@ -1636,25 +1646,26 @@ void Achievements::HandleLeaderboardTrackerShowEvent(const rc_client_event_t* ev
|
||||
void Achievements::HandleLeaderboardTrackerHideEvent(const rc_client_event_t* event)
|
||||
{
|
||||
const u32 id = event->leaderboard_tracker->id;
|
||||
DEV_LOG("Hiding leaderboard tracker: {}", id);
|
||||
|
||||
auto it = std::find_if(s_state.active_leaderboard_trackers.begin(), s_state.active_leaderboard_trackers.end(),
|
||||
[id](const auto& it) { return it.tracker_id == id; });
|
||||
if (it == s_state.active_leaderboard_trackers.end())
|
||||
return;
|
||||
|
||||
DEV_LOG("Hiding leaderboard tracker: {}", id);
|
||||
it->active = false;
|
||||
}
|
||||
|
||||
void Achievements::HandleLeaderboardTrackerUpdateEvent(const rc_client_event_t* event)
|
||||
{
|
||||
const u32 id = event->leaderboard_tracker->id;
|
||||
DEV_LOG("Updating leaderboard tracker: {}: {}", id, event->leaderboard_tracker->display);
|
||||
|
||||
auto it = std::find_if(s_state.active_leaderboard_trackers.begin(), s_state.active_leaderboard_trackers.end(),
|
||||
[id](const auto& it) { return it.tracker_id == id; });
|
||||
if (it == s_state.active_leaderboard_trackers.end())
|
||||
return;
|
||||
|
||||
DEV_LOG("Updating leaderboard tracker: {}: {}", event->leaderboard_tracker->id, event->leaderboard_tracker->display);
|
||||
|
||||
it->text = event->leaderboard_tracker->display;
|
||||
it->active = true;
|
||||
}
|
||||
@@ -1670,9 +1681,27 @@ void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_
|
||||
return;
|
||||
}
|
||||
|
||||
std::string badge_path = GetAchievementBadgePath(event->achievement, false);
|
||||
|
||||
// we still track these even if the option is disabled, so that they can be displayed in the pause menu
|
||||
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification)
|
||||
{
|
||||
std::string description = fmt::format(TRANSLATE_FS("Achievements", "Challenge Started: {}"),
|
||||
event->achievement->description ? event->achievement->description : "");
|
||||
GPUThread::RunOnThread([title = std::string(event->achievement->title), description = std::move(description),
|
||||
badge_path, id = event->achievement->id]() mutable {
|
||||
if (!FullscreenUI::Initialize())
|
||||
return;
|
||||
|
||||
ImGuiFullscreen::AddNotification(fmt::format("AchievementChallenge{}", id), LEADERBOARD_STARTED_NOTIFICATION_TIME,
|
||||
std::move(title), std::move(description), std::move(badge_path));
|
||||
});
|
||||
}
|
||||
|
||||
s_state.active_challenge_indicators.push_back(
|
||||
AchievementChallengeIndicator{.achievement = event->achievement,
|
||||
.badge_path = GetAchievementBadgePath(event->achievement, false),
|
||||
.badge_path = std::move(badge_path),
|
||||
.time_remaining = LEADERBOARD_STARTED_NOTIFICATION_TIME,
|
||||
.opacity = 0.0f,
|
||||
.active = true});
|
||||
|
||||
@@ -1688,6 +1717,15 @@ void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_
|
||||
return;
|
||||
|
||||
DEV_LOG("Hide challenge indicator for {} ({})", event->achievement->id, event->achievement->title);
|
||||
|
||||
if (g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Notification ||
|
||||
g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::Disabled)
|
||||
{
|
||||
// remove it here, because it won't naturally decay
|
||||
s_state.active_challenge_indicators.erase(it);
|
||||
return;
|
||||
}
|
||||
|
||||
it->active = false;
|
||||
}
|
||||
|
||||
@@ -1696,6 +1734,9 @@ void Achievements::HandleAchievementProgressIndicatorShowEvent(const rc_client_e
|
||||
DEV_LOG("Showing progress indicator: {} ({}): {}", event->achievement->id, event->achievement->title,
|
||||
event->achievement->measured_progress);
|
||||
|
||||
if (!g_settings.achievements_progress_indicators)
|
||||
return;
|
||||
|
||||
if (!s_state.active_progress_indicator.has_value())
|
||||
s_state.active_progress_indicator.emplace();
|
||||
|
||||
@@ -1711,6 +1752,13 @@ void Achievements::HandleAchievementProgressIndicatorHideEvent(const rc_client_e
|
||||
return;
|
||||
|
||||
DEV_LOG("Hiding progress indicator");
|
||||
|
||||
if (!g_settings.achievements_progress_indicators)
|
||||
{
|
||||
s_state.active_progress_indicator.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
s_state.active_progress_indicator->active = false;
|
||||
}
|
||||
|
||||
@@ -1718,6 +1766,9 @@ void Achievements::HandleAchievementProgressIndicatorUpdateEvent(const rc_client
|
||||
{
|
||||
DEV_LOG("Updating progress indicator: {} ({}): {}", event->achievement->id, event->achievement->title,
|
||||
event->achievement->measured_progress);
|
||||
if (!s_state.active_progress_indicator.has_value())
|
||||
return;
|
||||
|
||||
s_state.active_progress_indicator->achievement = event->achievement;
|
||||
s_state.active_progress_indicator->active = true;
|
||||
}
|
||||
@@ -2286,11 +2337,10 @@ void Achievements::ClearUIState()
|
||||
s_state.achievement_nearest_completion.reset();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static float IndicatorOpacity(float delta_time, T& i)
|
||||
static float IndicatorOpacity(float delta_time, bool active, float& opacity)
|
||||
{
|
||||
float target, rate;
|
||||
if (i.active)
|
||||
if (active)
|
||||
{
|
||||
target = 1.0f;
|
||||
rate = Achievements::INDICATOR_FADE_IN_TIME;
|
||||
@@ -2301,10 +2351,10 @@ static float IndicatorOpacity(float delta_time, T& i)
|
||||
rate = -Achievements::INDICATOR_FADE_OUT_TIME;
|
||||
}
|
||||
|
||||
if (i.opacity != target)
|
||||
i.opacity = ImSaturate(i.opacity + (delta_time / rate));
|
||||
if (opacity != target)
|
||||
opacity = ImSaturate(opacity + (delta_time / rate));
|
||||
|
||||
return i.opacity;
|
||||
return opacity;
|
||||
}
|
||||
|
||||
void Achievements::DrawGameOverlays()
|
||||
@@ -2314,7 +2364,7 @@ void Achievements::DrawGameOverlays()
|
||||
using ImGuiFullscreen::RenderShadowedTextClipped;
|
||||
using ImGuiFullscreen::UIStyle;
|
||||
|
||||
if (!HasActiveGame() || !g_settings.achievements_overlays)
|
||||
if (!HasActiveGame())
|
||||
return;
|
||||
|
||||
const auto lock = GetLock();
|
||||
@@ -2331,15 +2381,26 @@ void Achievements::DrawGameOverlays()
|
||||
ImVec2 position = ImVec2(io.DisplaySize.x - margin, io.DisplaySize.y - margin);
|
||||
ImDrawList* dl = ImGui::GetBackgroundDrawList();
|
||||
|
||||
if (!s_state.active_challenge_indicators.empty())
|
||||
if (!s_state.active_challenge_indicators.empty() &&
|
||||
(g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::PersistentIcon ||
|
||||
g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon))
|
||||
{
|
||||
const bool use_time_remaining =
|
||||
(g_settings.achievements_challenge_indicator_mode == AchievementChallengeIndicatorMode::TemporaryIcon);
|
||||
const float x_advance = image_size.x + spacing;
|
||||
ImVec2 current_position = ImVec2(position.x - image_size.x, position.y - image_size.y);
|
||||
|
||||
for (auto it = s_state.active_challenge_indicators.begin(); it != s_state.active_challenge_indicators.end();)
|
||||
{
|
||||
AchievementChallengeIndicator& indicator = *it;
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, indicator);
|
||||
bool active = indicator.active;
|
||||
if (use_time_remaining)
|
||||
{
|
||||
indicator.time_remaining = std::max(indicator.time_remaining - io.DeltaTime, 0.0f);
|
||||
active = (indicator.time_remaining > 0.0f);
|
||||
}
|
||||
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, active, indicator.opacity);
|
||||
|
||||
GPUTexture* badge = ImGuiFullscreen::GetCachedTextureAsync(indicator.badge_path);
|
||||
if (badge)
|
||||
@@ -2366,7 +2427,7 @@ void Achievements::DrawGameOverlays()
|
||||
if (s_state.active_progress_indicator.has_value())
|
||||
{
|
||||
AchievementProgressIndicator& indicator = s_state.active_progress_indicator.value();
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, indicator);
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, indicator.active, indicator.opacity);
|
||||
|
||||
const std::string_view text = s_state.active_progress_indicator->achievement->measured_progress;
|
||||
const ImVec2 text_size = UIStyle.Font->CalcTextSizeA(UIStyle.MediumFontSize, UIStyle.NormalFontWeight, FLT_MAX,
|
||||
@@ -2408,7 +2469,7 @@ void Achievements::DrawGameOverlays()
|
||||
for (auto it = s_state.active_leaderboard_trackers.begin(); it != s_state.active_leaderboard_trackers.end();)
|
||||
{
|
||||
LeaderboardTrackerIndicator& indicator = *it;
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, indicator);
|
||||
const float opacity = IndicatorOpacity(io.DeltaTime, indicator.active, indicator.opacity);
|
||||
|
||||
TinyString width_string;
|
||||
width_string.append(ICON_FA_STOPWATCH);
|
||||
|
||||
@@ -6414,21 +6414,6 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
|
||||
}
|
||||
}
|
||||
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_BELL, "Achievement Notifications"),
|
||||
FSUI_VSTR("Displays popup messages on events such as achievement unlocks and leaderboard submissions."), "Cheevos",
|
||||
"Notifications", true, enabled);
|
||||
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_LIST_OL, "Leaderboard Notifications"),
|
||||
FSUI_VSTR("Displays popup messages when starting, submitting, or failing a leaderboard challenge."),
|
||||
"Cheevos", "LeaderboardNotifications", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_MUSIC, "Sound Effects"),
|
||||
FSUI_VSTR("Plays sound effects for events such as achievement unlocks and leaderboard submissions."), "Cheevos",
|
||||
"SoundEffects", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_WAND_MAGIC_SPARKLES, "Enable In-Game Overlays"),
|
||||
FSUI_VSTR("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active."),
|
||||
"Cheevos", "Overlays", true, enabled);
|
||||
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_ARROW_ROTATE_RIGHT, "Encore Mode"),
|
||||
FSUI_VSTR("When enabled, each session will behave as if no achievements have been unlocked."),
|
||||
"Cheevos", "EncoreMode", false, enabled);
|
||||
@@ -6442,6 +6427,34 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
|
||||
"tracked by RetroAchievements."),
|
||||
"Cheevos", "UnofficialTestMode", false, enabled);
|
||||
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_BELL, "Achievement Notifications"),
|
||||
FSUI_VSTR("Displays popup messages on events such as achievement unlocks and leaderboard submissions."), "Cheevos",
|
||||
"Notifications", true, enabled);
|
||||
DrawToggleSetting(bsi, FSUI_ICONVSTR(ICON_FA_LIST_OL, "Leaderboard Notifications"),
|
||||
FSUI_VSTR("Displays popup messages when starting, submitting, or failing a leaderboard challenge."),
|
||||
"Cheevos", "LeaderboardNotifications", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_CLOCK, "Leaderboard Trackers"),
|
||||
FSUI_VSTR("Shows a timer in the bottom-right corner of the screen when leaderboard challenges are active."),
|
||||
"Cheevos", "LeaderboardTrackers", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_MUSIC, "Sound Effects"),
|
||||
FSUI_VSTR("Plays sound effects for events such as achievement unlocks and leaderboard submissions."), "Cheevos",
|
||||
"SoundEffects", true, enabled);
|
||||
DrawToggleSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_BARS_PROGRESS, "Progress Indicators"),
|
||||
FSUI_VSTR(
|
||||
"Shows a popup in the lower-right corner of the screen when progress towards a measured achievement changes."),
|
||||
"Cheevos", "ProgressIndicators", true, enabled);
|
||||
DrawEnumSetting(
|
||||
bsi, FSUI_ICONVSTR(ICON_FA_TEMPERATURE_ARROW_UP, "Challenge Indicators"),
|
||||
FSUI_VSTR("Shows a notification or icons in the lower-right corner of the screen when a challenge/primed "
|
||||
"achievement is active."),
|
||||
"Cheevos", "ChallengeIndicatorMode", Settings::DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE,
|
||||
&Settings::ParseAchievementChallengeIndicatorMode, &Settings::GetAchievementChallengeIndicatorModeName,
|
||||
&Settings::GetAchievementChallengeIndicatorModeDisplayName, AchievementChallengeIndicatorMode::MaxCount, enabled);
|
||||
|
||||
if (!IsEditingGameSettings(bsi))
|
||||
{
|
||||
if (MenuButton(FSUI_ICONVSTR(ICON_FA_ARROWS_ROTATE, "Update Progress"),
|
||||
@@ -6461,7 +6474,8 @@ void FullscreenUI::DrawAchievementsSettingsPage(std::unique_lock<std::mutex>& se
|
||||
Host::NumberFormatType::LongDateTime,
|
||||
StringUtil::FromChars<s64>(bsi->GetTinyStringValue("Cheevos", "LoginTimestamp", "0")).value_or(0));
|
||||
MenuButtonWithoutSummary(
|
||||
SmallString::from_format(fmt::runtime(FSUI_ICONVSTR(ICON_FA_CLOCK, "Login token generated on {}")), ts_string),
|
||||
SmallString::from_format(fmt::runtime(FSUI_ICONVSTR(ICON_FA_USER_CLOCK, "Login token generated on {}")),
|
||||
ts_string),
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -9478,6 +9492,7 @@ TRANSLATE_NOOP("FullscreenUI", "CPU Emulation");
|
||||
TRANSLATE_NOOP("FullscreenUI", "CPU Mode");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Cancel");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Capture");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Challenge Indicators");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Change Disc");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Change Page");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Change Selection");
|
||||
@@ -9608,7 +9623,6 @@ TRANSLATE_NOOP("FullscreenUI", "Enable Discord Presence");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable Fast Boot");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable GPU-Based Validation");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable GPU-based validation when supported by the host's renderer API. Only for developer use.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable In-Game Overlays");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable Overclocking");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable Post Processing");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Enable Recompiler Block Linking");
|
||||
@@ -9739,6 +9753,7 @@ TRANSLATE_NOOP("FullscreenUI", "Launch a game by selecting a file/disc image.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Launch a game from a file, disc, or starts the console without any disc inserted.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Launch a game from images scanned from your game directories.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Leaderboard Notifications");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Leaderboard Trackers");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Leaderboards");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Left: ");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Light");
|
||||
@@ -9855,6 +9870,7 @@ TRANSLATE_NOOP("FullscreenUI", "Press To Toggle");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Pressure");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Prevents the emulator from producing any audible sound.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Prevents the screen saver from activating and the host from sleeping while emulation is running.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Progress Indicators");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Provides vibration and LED control support over Bluetooth.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Purple Rain");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Push a controller button or axis now.");
|
||||
@@ -9989,9 +10005,11 @@ TRANSLATE_NOOP("FullscreenUI", "Show Resolution");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Speed");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Show Status Indicators");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a background image or shader when a game isn't running. Backgrounds are located in resources/fullscreenui/backgrounds in the data directory.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a notification or icons in the lower-right corner of the screen when a challenge/primed achievement is active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a popup in the lower-right corner of the screen when progress towards a measured achievement changes.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a timer in the bottom-right corner of the screen when leaderboard challenges are active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows a visual history of frame times in the upper-left corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows enhancement settings in the bottom-right corner of the screen.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows information about input and audio latency in the top-right corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows information about the emulated GPU in the top-right corner of the display.");
|
||||
TRANSLATE_NOOP("FullscreenUI", "Shows on-screen-display messages when events occur.");
|
||||
|
||||
@@ -437,18 +437,27 @@ void Settings::Load(const SettingsInterface& si, const SettingsInterface& contro
|
||||
|
||||
achievements_enabled = si.GetBoolValue("Cheevos", "Enabled", false);
|
||||
achievements_hardcore_mode = si.GetBoolValue("Cheevos", "ChallengeMode", false);
|
||||
achievements_notifications = si.GetBoolValue("Cheevos", "Notifications", true);
|
||||
achievements_leaderboard_notifications = si.GetBoolValue("Cheevos", "LeaderboardNotifications", true);
|
||||
achievements_sound_effects = si.GetBoolValue("Cheevos", "SoundEffects", true);
|
||||
achievements_overlays = si.GetBoolValue("Cheevos", "Overlays", true);
|
||||
achievements_encore_mode = si.GetBoolValue("Cheevos", "EncoreMode", false);
|
||||
achievements_spectator_mode = si.GetBoolValue("Cheevos", "SpectatorMode", false);
|
||||
achievements_unofficial_test_mode = si.GetBoolValue("Cheevos", "UnofficialTestMode", false);
|
||||
achievements_use_raintegration = si.GetBoolValue("Cheevos", "UseRAIntegration", false);
|
||||
achievements_notifications = si.GetBoolValue("Cheevos", "Notifications", true);
|
||||
achievements_leaderboard_notifications = si.GetBoolValue("Cheevos", "LeaderboardNotifications", true);
|
||||
achievements_leaderboard_trackers = si.GetBoolValue("Cheevos", "LeaderboardTrackers", true);
|
||||
achievements_sound_effects = si.GetBoolValue("Cheevos", "SoundEffects", true);
|
||||
achievements_progress_indicators = si.GetBoolValue("Cheevos", "ProgressIndicators", true);
|
||||
achievements_challenge_indicator_mode =
|
||||
ParseAchievementChallengeIndicatorMode(
|
||||
si.GetStringValue("Cheevos", "ChallengeIndicatorMode",
|
||||
GetAchievementChallengeIndicatorModeName(DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE))
|
||||
.c_str())
|
||||
.value_or(DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE);
|
||||
achievements_notification_duration =
|
||||
si.GetIntValue("Cheevos", "NotificationsDuration", DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME);
|
||||
Truncate8(std::min<u32>(si.GetUIntValue("Cheevos", "NotificationsDuration", DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME),
|
||||
std::numeric_limits<u8>::max()));
|
||||
achievements_leaderboard_duration =
|
||||
si.GetIntValue("Cheevos", "LeaderboardsDuration", DEFAULT_LEADERBOARD_NOTIFICATION_TIME);
|
||||
Truncate8(std::min<u32>(si.GetUIntValue("Cheevos", "LeaderboardsDuration", DEFAULT_LEADERBOARD_NOTIFICATION_TIME),
|
||||
std::numeric_limits<u8>::max()));
|
||||
|
||||
#ifndef __ANDROID__
|
||||
enable_gdb_server = si.GetBoolValue("Debug", "EnableGDBServer");
|
||||
@@ -737,16 +746,19 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
|
||||
|
||||
si.SetBoolValue("Cheevos", "Enabled", achievements_enabled);
|
||||
si.SetBoolValue("Cheevos", "ChallengeMode", achievements_hardcore_mode);
|
||||
si.SetBoolValue("Cheevos", "Notifications", achievements_notifications);
|
||||
si.SetBoolValue("Cheevos", "LeaderboardNotifications", achievements_leaderboard_notifications);
|
||||
si.SetBoolValue("Cheevos", "SoundEffects", achievements_sound_effects);
|
||||
si.SetBoolValue("Cheevos", "Overlays", achievements_overlays);
|
||||
si.SetBoolValue("Cheevos", "EncoreMode", achievements_encore_mode);
|
||||
si.SetBoolValue("Cheevos", "SpectatorMode", achievements_spectator_mode);
|
||||
si.SetBoolValue("Cheevos", "UnofficialTestMode", achievements_unofficial_test_mode);
|
||||
si.SetBoolValue("Cheevos", "UseRAIntegration", achievements_use_raintegration);
|
||||
si.SetIntValue("Cheevos", "NotificationsDuration", achievements_notification_duration);
|
||||
si.SetIntValue("Cheevos", "LeaderboardsDuration", achievements_leaderboard_duration);
|
||||
si.SetBoolValue("Cheevos", "Notifications", achievements_notifications);
|
||||
si.SetBoolValue("Cheevos", "LeaderboardNotifications", achievements_leaderboard_notifications);
|
||||
si.SetBoolValue("Cheevos", "LeaderboardTrackers", achievements_leaderboard_trackers);
|
||||
si.SetBoolValue("Cheevos", "SoundEffects", achievements_sound_effects);
|
||||
si.SetBoolValue("Cheevos", "ProgressIndicators", achievements_progress_indicators);
|
||||
si.SetStringValue("Cheevos", "ChallengeIndicatorMode",
|
||||
GetAchievementChallengeIndicatorModeName(achievements_challenge_indicator_mode));
|
||||
si.SetUIntValue("Cheevos", "NotificationsDuration", achievements_notification_duration);
|
||||
si.SetUIntValue("Cheevos", "LeaderboardsDuration", achievements_leaderboard_duration);
|
||||
|
||||
#ifndef __ANDROID__
|
||||
si.SetBoolValue("Debug", "EnableGDBServer", enable_gdb_server);
|
||||
@@ -2156,6 +2168,49 @@ std::optional<DisplayScreenshotFormat> Settings::GetDisplayScreenshotFormatFromF
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static constexpr const std::array s_achievement_challenge_indicator_mode_names = {
|
||||
"Disabled",
|
||||
"PersistentIcon",
|
||||
"TemporaryIcon",
|
||||
"Notification",
|
||||
};
|
||||
static_assert(s_achievement_challenge_indicator_mode_names.size() ==
|
||||
static_cast<size_t>(AchievementChallengeIndicatorMode::MaxCount));
|
||||
static constexpr const std::array s_achievement_challenge_indicator_mode_display_names = {
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Disabled", "AchievementChallengeIndicatorMode"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Show Persistent Icons", "AchievementChallengeIndicatorMode"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Show Temporary Icons", "AchievementChallengeIndicatorMode"),
|
||||
TRANSLATE_DISAMBIG_NOOP("Settings", "Show Notifications", "AchievementChallengeIndicatorMode"),
|
||||
};
|
||||
static_assert(s_achievement_challenge_indicator_mode_display_names.size() ==
|
||||
static_cast<size_t>(AchievementChallengeIndicatorMode::MaxCount));
|
||||
|
||||
std::optional<AchievementChallengeIndicatorMode> Settings::ParseAchievementChallengeIndicatorMode(const char* str)
|
||||
{
|
||||
int index = 0;
|
||||
for (const char* name : s_achievement_challenge_indicator_mode_names)
|
||||
{
|
||||
if (StringUtil::Strcasecmp(name, str) == 0)
|
||||
return static_cast<AchievementChallengeIndicatorMode>(index);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* Settings::GetAchievementChallengeIndicatorModeName(AchievementChallengeIndicatorMode mode)
|
||||
{
|
||||
return s_achievement_challenge_indicator_mode_names[static_cast<size_t>(mode)];
|
||||
}
|
||||
|
||||
const char* Settings::GetAchievementChallengeIndicatorModeDisplayName(AchievementChallengeIndicatorMode mode)
|
||||
{
|
||||
return Host::TranslateToCString("Settings",
|
||||
s_achievement_challenge_indicator_mode_display_names[static_cast<size_t>(mode)],
|
||||
"AchievementChallengeIndicatorMode");
|
||||
}
|
||||
|
||||
static constexpr const std::array s_memory_card_type_names = {
|
||||
"None", "Shared", "PerGame", "PerGameTitle", "PerGameFileTitle", "NonPersistent",
|
||||
};
|
||||
|
||||
@@ -347,16 +347,19 @@ struct Settings : public GPUSettings
|
||||
// achievements
|
||||
bool achievements_enabled : 1 = false;
|
||||
bool achievements_hardcore_mode : 1 = false;
|
||||
bool achievements_notifications : 1 = true;
|
||||
bool achievements_leaderboard_notifications : 1 = true;
|
||||
bool achievements_sound_effects : 1 = true;
|
||||
bool achievements_overlays : 1 = true;
|
||||
bool achievements_encore_mode : 1 = false;
|
||||
bool achievements_spectator_mode : 1 = false;
|
||||
bool achievements_unofficial_test_mode : 1 = false;
|
||||
bool achievements_use_raintegration : 1 = false;
|
||||
s32 achievements_notification_duration = DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME;
|
||||
s32 achievements_leaderboard_duration = DEFAULT_LEADERBOARD_NOTIFICATION_TIME;
|
||||
bool achievements_notifications : 1 = true;
|
||||
bool achievements_leaderboard_notifications : 1 = true;
|
||||
bool achievements_leaderboard_trackers : 1 = true;
|
||||
bool achievements_sound_effects : 1 = true;
|
||||
bool achievements_progress_indicators : 1 = true;
|
||||
AchievementChallengeIndicatorMode achievements_challenge_indicator_mode =
|
||||
DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE;
|
||||
u8 achievements_notification_duration = DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME;
|
||||
u8 achievements_leaderboard_duration = DEFAULT_LEADERBOARD_NOTIFICATION_TIME;
|
||||
|
||||
float emulation_speed = 1.0f;
|
||||
float fast_forward_speed = 0.0f;
|
||||
@@ -546,6 +549,10 @@ struct Settings : public GPUSettings
|
||||
static const char* GetDisplayScreenshotModeName(DisplayScreenshotMode mode);
|
||||
static const char* GetDisplayScreenshotModeDisplayName(DisplayScreenshotMode mode);
|
||||
|
||||
static std::optional<AchievementChallengeIndicatorMode> ParseAchievementChallengeIndicatorMode(const char* str);
|
||||
static const char* GetAchievementChallengeIndicatorModeName(AchievementChallengeIndicatorMode mode);
|
||||
static const char* GetAchievementChallengeIndicatorModeDisplayName(AchievementChallengeIndicatorMode mode);
|
||||
|
||||
static std::optional<DisplayScreenshotFormat> ParseDisplayScreenshotFormat(const char* str);
|
||||
static const char* GetDisplayScreenshotFormatName(DisplayScreenshotFormat mode);
|
||||
static const char* GetDisplayScreenshotFormatDisplayName(DisplayScreenshotFormat mode);
|
||||
@@ -602,8 +609,10 @@ struct Settings : public GPUSettings
|
||||
static constexpr MultitapMode DEFAULT_MULTITAP_MODE = MultitapMode::Disabled;
|
||||
static constexpr PIODeviceType DEFAULT_PIO_DEVICE_TYPE = PIODeviceType::None;
|
||||
|
||||
static constexpr s32 DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME = 5;
|
||||
static constexpr s32 DEFAULT_LEADERBOARD_NOTIFICATION_TIME = 10;
|
||||
static constexpr AchievementChallengeIndicatorMode DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE =
|
||||
AchievementChallengeIndicatorMode::PersistentIcon;
|
||||
static constexpr u8 DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME = 5;
|
||||
static constexpr u8 DEFAULT_LEADERBOARD_NOTIFICATION_TIME = 10;
|
||||
|
||||
static constexpr Log::Level DEFAULT_LOG_LEVEL = Log::Level::Info;
|
||||
|
||||
|
||||
@@ -228,6 +228,16 @@ enum class DisplayScreenshotFormat : u8
|
||||
Count
|
||||
};
|
||||
|
||||
enum class AchievementChallengeIndicatorMode : u8
|
||||
{
|
||||
Disabled,
|
||||
PersistentIcon,
|
||||
TemporaryIcon,
|
||||
Notification,
|
||||
|
||||
MaxCount
|
||||
};
|
||||
|
||||
enum class ControllerType : u8
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -27,38 +27,33 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
|
||||
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enable, "Cheevos", "Enabled", false);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hardcoreMode, "Cheevos", "ChallengeMode", false);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.achievementNotifications, "Cheevos", "Notifications", true);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardNotifications, "Cheevos",
|
||||
"LeaderboardNotifications", true);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.overlays, "Cheevos", "Overlays", true);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.encoreMode, "Cheevos", "EncoreMode", false);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.spectatorMode, "Cheevos", "SpectatorMode", false);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.unofficialAchievements, "Cheevos", "UnofficialTestMode",
|
||||
false);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.achievementNotifications, "Cheevos", "Notifications", true);
|
||||
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.achievementNotificationsDuration, "Cheevos",
|
||||
"NotificationsDuration",
|
||||
Settings::DEFAULT_ACHIEVEMENT_NOTIFICATION_TIME);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardNotifications, "Cheevos",
|
||||
"LeaderboardNotifications", true);
|
||||
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.leaderboardNotificationsDuration, "Cheevos",
|
||||
"LeaderboardsDuration",
|
||||
Settings::DEFAULT_LEADERBOARD_NOTIFICATION_TIME);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.leaderboardTrackers, "Cheevos", "LeaderboardTrackers", true);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.soundEffects, "Cheevos", "SoundEffects", true);
|
||||
SettingWidgetBinder::BindWidgetToEnumSetting(
|
||||
sif, m_ui.challengeIndicatorMode, "Cheevos", "ChallengeIndicatorMode",
|
||||
&Settings::ParseAchievementChallengeIndicatorMode, &Settings::GetAchievementChallengeIndicatorModeName,
|
||||
&Settings::GetAchievementChallengeIndicatorModeDisplayName, Settings::DEFAULT_ACHIEVEMENT_CHALLENGE_INDICATOR_MODE,
|
||||
AchievementChallengeIndicatorMode::MaxCount);
|
||||
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.progressIndicators, "Cheevos", "ProgressIndicators", true);
|
||||
|
||||
dialog->registerWidgetHelp(m_ui.enable, tr("Enable Achievements"), tr("Unchecked"),
|
||||
tr("When enabled and logged in, DuckStation will scan for achievements on startup."));
|
||||
dialog->registerWidgetHelp(m_ui.hardcoreMode, tr("Enable Hardcore Mode"), tr("Unchecked"),
|
||||
tr("\"Challenge\" mode for achievements, including leaderboard tracking. Disables save "
|
||||
"state, cheats, and slowdown functions."));
|
||||
dialog->registerWidgetHelp(m_ui.achievementNotifications, tr("Show Achievement Notifications"), tr("Checked"),
|
||||
tr("Displays popup messages on events such as achievement unlocks and game completion."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.leaderboardNotifications, tr("Show Leaderboard Notifications"), tr("Checked"),
|
||||
tr("Displays popup messages when starting, submitting, or failing a leaderboard challenge."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"),
|
||||
tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.overlays, tr("Enable In-Game Overlays"), tr("Checked"),
|
||||
tr("Shows icons in the lower-right corner of the screen when a challenge/primed achievement is active."));
|
||||
dialog->registerWidgetHelp(m_ui.encoreMode, tr("Enable Encore Mode"), tr("Unchecked"),
|
||||
tr("When enabled, each session will behave as if no achievements have been unlocked."));
|
||||
dialog->registerWidgetHelp(m_ui.spectatorMode, tr("Enable Spectator Mode"), tr("Unchecked"),
|
||||
@@ -68,6 +63,23 @@ AchievementSettingsWidget::AchievementSettingsWidget(SettingsWindow* dialog, QWi
|
||||
m_ui.unofficialAchievements, tr("Test Unofficial Achievements"), tr("Unchecked"),
|
||||
tr("When enabled, DuckStation will list achievements from unofficial sets. Please note that these achievements are "
|
||||
"not tracked by RetroAchievements, so they unlock every time."));
|
||||
dialog->registerWidgetHelp(m_ui.achievementNotifications, tr("Show Achievement Notifications"), tr("Checked"),
|
||||
tr("Displays popup messages on events such as achievement unlocks and game completion."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.leaderboardNotifications, tr("Show Leaderboard Notifications"), tr("Checked"),
|
||||
tr("Displays popup messages when starting, submitting, or failing a leaderboard challenge."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.leaderboardTrackers, tr("Show Leaderboard Trackers"), tr("Checked"),
|
||||
tr("Shows a timer in the bottom-right corner of the screen when leaderboard challenges are active."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.soundEffects, tr("Enable Sound Effects"), tr("Checked"),
|
||||
tr("Plays sound effects for events such as achievement unlocks and leaderboard submissions."));
|
||||
dialog->registerWidgetHelp(m_ui.challengeIndicatorMode, tr("Challenge Indicators"), tr("Show Persistent Icons"),
|
||||
tr("Shows a notification or icons in the lower-right corner of the screen when a "
|
||||
"challenge/primed achievement is active."));
|
||||
dialog->registerWidgetHelp(
|
||||
m_ui.progressIndicators, tr("Show Progress Indicators"), tr("Checked"),
|
||||
tr("Shows a popup in the lower-right corner of the screen when progress towards a measured achievement changes."));
|
||||
|
||||
connect(m_ui.enable, &QCheckBox::checkStateChanged, this, &AchievementSettingsWidget::updateEnableState);
|
||||
connect(m_ui.hardcoreMode, &QCheckBox::checkStateChanged, this, &AchievementSettingsWidget::updateEnableState);
|
||||
@@ -140,8 +152,11 @@ void AchievementSettingsWidget::updateEnableState()
|
||||
m_ui.achievementNotificationsDurationLabel->setEnabled(notifications);
|
||||
m_ui.leaderboardNotificationsDuration->setEnabled(lb_notifications);
|
||||
m_ui.leaderboardNotificationsDurationLabel->setEnabled(lb_notifications);
|
||||
m_ui.leaderboardTrackers->setEnabled(enabled);
|
||||
m_ui.soundEffects->setEnabled(enabled);
|
||||
m_ui.overlays->setEnabled(enabled);
|
||||
m_ui.challengeIndicatorMode->setEnabled(enabled);
|
||||
m_ui.challengeIndicatorModeLabel->setEnabled(enabled);
|
||||
m_ui.progressIndicators->setEnabled(enabled);
|
||||
m_ui.encoreMode->setEnabled(enabled);
|
||||
m_ui.spectatorMode->setEnabled(enabled);
|
||||
m_ui.unofficialAchievements->setEnabled(enabled);
|
||||
@@ -199,11 +214,10 @@ void AchievementSettingsWidget::updateLoginState()
|
||||
{
|
||||
const u64 login_unix_timestamp =
|
||||
StringUtil::FromChars<u64>(Host::GetBaseStringSettingValue("Cheevos", "LoginTimestamp", "0")).value_or(0);
|
||||
const QString login_timestamp = QtHost::FormatNumber(Host::NumberFormatType::ShortDateTime,
|
||||
static_cast<s64>(login_unix_timestamp));
|
||||
m_ui.loginStatus->setText(tr("Username: %1\nLogin token generated on %2.")
|
||||
.arg(QString::fromStdString(username))
|
||||
.arg(login_timestamp));
|
||||
const QString login_timestamp =
|
||||
QtHost::FormatNumber(Host::NumberFormatType::ShortDateTime, static_cast<s64>(login_unix_timestamp));
|
||||
m_ui.loginStatus->setText(
|
||||
tr("Username: %1\nLogin token generated on %2.").arg(QString::fromStdString(username)).arg(login_timestamp));
|
||||
m_ui.loginButton->setText(tr("Logout"));
|
||||
}
|
||||
else
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>674</width>
|
||||
<height>420</height>
|
||||
<height>479</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@@ -80,6 +80,13 @@
|
||||
<string>Notifications</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2" columnstretch="1,1">
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="leaderboardNotifications">
|
||||
<property name="text">
|
||||
<string>Show Leaderboard Notifications</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0">
|
||||
<item>
|
||||
@@ -165,30 +172,49 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="leaderboardNotifications">
|
||||
<property name="text">
|
||||
<string>Show Leaderboard Notifications</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="soundEffects">
|
||||
<property name="text">
|
||||
<string>Enable Sound Effects</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="overlays">
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="leaderboardTrackers">
|
||||
<property name="text">
|
||||
<string>Enable In-Game Overlays</string>
|
||||
<string>Show Leaderboard Trackers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Progress Tracking</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" columnstretch="1,1">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="challengeIndicatorModeLabel">
|
||||
<property name="text">
|
||||
<string>Challenge Indicators:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="progressIndicators">
|
||||
<property name="text">
|
||||
<string>Show Progress Indicators</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="challengeIndicatorMode"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="loginBox">
|
||||
<property name="title">
|
||||
|
||||
Reference in New Issue
Block a user