diff --git a/skeleton/SYSTEM/res/assets.svg b/skeleton/SYSTEM/res/assets.svg
index 12a88dcec..d2ca33f3d 100644
--- a/skeleton/SYSTEM/res/assets.svg
+++ b/skeleton/SYSTEM/res/assets.svg
@@ -717,4 +717,9 @@
id="g4"
transform="matrix(0.04561003,0,0,0.04561003,370.08438,461.89281)">
+ id="path1-22" />
diff --git a/skeleton/SYSTEM/res/assets@1x.png b/skeleton/SYSTEM/res/assets@1x.png
index 1b709919c..255f4c272 100644
Binary files a/skeleton/SYSTEM/res/assets@1x.png and b/skeleton/SYSTEM/res/assets@1x.png differ
diff --git a/skeleton/SYSTEM/res/assets@2x.png b/skeleton/SYSTEM/res/assets@2x.png
index abf458a88..2ca6facb2 100644
Binary files a/skeleton/SYSTEM/res/assets@2x.png and b/skeleton/SYSTEM/res/assets@2x.png differ
diff --git a/skeleton/SYSTEM/res/assets@3x.png b/skeleton/SYSTEM/res/assets@3x.png
index 8cd6462c0..69153987e 100644
Binary files a/skeleton/SYSTEM/res/assets@3x.png and b/skeleton/SYSTEM/res/assets@3x.png differ
diff --git a/skeleton/SYSTEM/res/assets@4x.png b/skeleton/SYSTEM/res/assets@4x.png
index 941d40d55..fe456303f 100644
Binary files a/skeleton/SYSTEM/res/assets@4x.png and b/skeleton/SYSTEM/res/assets@4x.png differ
diff --git a/workspace/all/common/api.c b/workspace/all/common/api.c
index 19f3f5439..e782179c6 100644
--- a/workspace/all/common/api.c
+++ b/workspace/all/common/api.c
@@ -437,6 +437,7 @@ SDL_Surface *GFX_init(int mode)
asset_rects[ASSET_WIFI_OFF] = (SDL_Rect){SCALE4(40, 104, 12, 12)};
asset_rects[ASSET_CHECKCIRCLE] = (SDL_Rect){SCALE4(1, 117, 10, 10)};
asset_rects[ASSET_LOCK] = (SDL_Rect){SCALE4(12, 116, 8, 11)};
+ asset_rects[ASSET_TROPHY] = (SDL_Rect){SCALE4(78, 117, 10, 11)};
asset_rects[ASSET_HOLE] = (SDL_Rect){SCALE4(1, 63, 20, 20)};
asset_rects[ASSET_GAMEPAD] = (SDL_Rect){SCALE4(91, 51, 17, 10)};
asset_rects[ASSET_SETTINGS] = (SDL_Rect){SCALE4(21, 117, 10, 10)};
diff --git a/workspace/all/common/api.h b/workspace/all/common/api.h
index 828af8271..5e6c362f8 100644
--- a/workspace/all/common/api.h
+++ b/workspace/all/common/api.h
@@ -161,6 +161,7 @@ enum {
ASSET_CHECKCIRCLE,
ASSET_LOCK,
+ ASSET_TROPHY,
ASSET_SETTINGS,
ASSET_STORE,
ASSET_GAMEPAD,
diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c
index e1ec3ba7b..aea4cb496 100644
--- a/workspace/all/common/config.c
+++ b/workspace/all/common/config.c
@@ -100,6 +100,7 @@ void CFG_defaults(NextUISettings *cfg)
.raServerUsername = CFG_DEFAULT_RA_SERVER_USERNAME,
.raAuthenticated = CFG_DEFAULT_RA_AUTHENTICATED,
.raShowNotifications = CFG_DEFAULT_RA_SHOW_NOTIFICATIONS,
+ .raShowChallengeIndicators = CFG_DEFAULT_RA_SHOW_CHALLENGE_INDICATORS,
.raNotificationDuration = CFG_DEFAULT_RA_NOTIFICATION_DURATION,
.raProgressNotificationDuration = CFG_DEFAULT_RA_PROGRESS_NOTIFICATION_DURATION,
.raAchievementSortOrder = CFG_DEFAULT_RA_ACHIEVEMENT_SORT_ORDER,
@@ -489,6 +490,11 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
CFG_setRAShowNotifications((bool)temp_value);
continue;
}
+ if (sscanf(line, "raShowChallengeIndicators=%i", &temp_value) == 1)
+ {
+ CFG_setRAShowChallengeIndicators((bool)temp_value);
+ continue;
+ }
if (sscanf(line, "raNotificationDuration=%i", &temp_value) == 1)
{
CFG_setRANotificationDuration(temp_value);
@@ -1246,6 +1252,17 @@ void CFG_setRAShowNotifications(bool show)
CFG_sync();
}
+bool CFG_getRAShowChallengeIndicators(void)
+{
+ return settings.raShowChallengeIndicators;
+}
+
+void CFG_setRAShowChallengeIndicators(bool show)
+{
+ settings.raShowChallengeIndicators = show;
+ CFG_sync();
+}
+
int CFG_getRANotificationDuration(void)
{
return settings.raNotificationDuration;
@@ -1541,6 +1558,10 @@ void CFG_get(const char *key, char *value)
{
sprintf(value, "%i", (int)(CFG_getRAShowNotifications()));
}
+ else if (strcmp(key, "raShowChallengeIndicators") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getRAShowChallengeIndicators()));
+ }
else if (strcmp(key, "raNotificationDuration") == 0)
{
sprintf(value, "%i", CFG_getRANotificationDuration());
@@ -1660,6 +1681,7 @@ void CFG_sync(void)
fprintf(file, "raServerUsername=%s\n", settings.raServerUsername);
fprintf(file, "raAuthenticated=%i\n", settings.raAuthenticated);
fprintf(file, "raShowNotifications=%i\n", settings.raShowNotifications);
+ fprintf(file, "raShowChallengeIndicators=%i\n", settings.raShowChallengeIndicators);
fprintf(file, "raNotificationDuration=%i\n", settings.raNotificationDuration);
fprintf(file, "raProgressNotificationDuration=%i\n", settings.raProgressNotificationDuration);
fprintf(file, "raAchievementSortOrder=%i\n", settings.raAchievementSortOrder);
@@ -1733,6 +1755,7 @@ void CFG_print(void)
printf("\t\"raServerUsername\": \"%s\",\n", settings.raServerUsername);
printf("\t\"raAuthenticated\": %i,\n", settings.raAuthenticated);
printf("\t\"raShowNotifications\": %i,\n", settings.raShowNotifications);
+ printf("\t\"raShowChallengeIndicators\": %i,\n", settings.raShowChallengeIndicators);
printf("\t\"raNotificationDuration\": %i,\n", settings.raNotificationDuration);
printf("\t\"raProgressNotificationDuration\": %i,\n", settings.raProgressNotificationDuration);
printf("\t\"raAchievementSortOrder\": %i,\n", settings.raAchievementSortOrder);
diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h
index cdd577866..bad3f5fc0 100644
--- a/workspace/all/common/config.h
+++ b/workspace/all/common/config.h
@@ -187,6 +187,7 @@ typedef struct
char raServerUsername[64]; // Server's internal username (from avatar URL, used for sync hash)
bool raAuthenticated; // Whether we have a valid token
bool raShowNotifications; // Show achievement unlock notifications
+ bool raShowChallengeIndicators; // Show on-screen indicator while a challenge achievement is active
int raNotificationDuration; // Duration for achievement notifications (1-5 seconds)
int raProgressNotificationDuration; // Duration for progress notifications (0-5 seconds, 0 = disabled)
int raAchievementSortOrder; // Sort order for achievements list (RA_SORT_* enum)
@@ -267,6 +268,7 @@ typedef struct
#define CFG_DEFAULT_RA_SERVER_USERNAME ""
#define CFG_DEFAULT_RA_AUTHENTICATED false
#define CFG_DEFAULT_RA_SHOW_NOTIFICATIONS true
+#define CFG_DEFAULT_RA_SHOW_CHALLENGE_INDICATORS true
#define CFG_DEFAULT_RA_NOTIFICATION_DURATION 3
#define CFG_DEFAULT_RA_PROGRESS_NOTIFICATION_DURATION 1
#define CFG_DEFAULT_RA_ACHIEVEMENT_SORT_ORDER RA_SORT_UNLOCKED_FIRST
@@ -469,6 +471,8 @@ bool CFG_getRAAuthenticated(void);
void CFG_setRAAuthenticated(bool authenticated);
bool CFG_getRAShowNotifications(void);
void CFG_setRAShowNotifications(bool show);
+bool CFG_getRAShowChallengeIndicators(void);
+void CFG_setRAShowChallengeIndicators(bool show);
int CFG_getRANotificationDuration(void);
void CFG_setRANotificationDuration(int seconds);
int CFG_getRAProgressNotificationDuration(void);
diff --git a/workspace/all/common/notification.c b/workspace/all/common/notification.c
index f8d454f4a..3b2512999 100644
--- a/workspace/all/common/notification.c
+++ b/workspace/all/common/notification.c
@@ -79,6 +79,23 @@ typedef struct {
static ProgressIndicatorState progress_state = {0};
static SDL_mutex* progress_mutex = NULL;
+///////////////////////////////
+// Challenge indicator state
+//
+// Active "challenge" achievements (tracked by id so SHOW/HIDE are idempotent).
+// Written from the rcheevos event handler (main thread) and read during
+// rendering; guarded by challenge_mutex to match the progress-indicator
+// pattern and stay safe regardless of which thread renders.
+///////////////////////////////
+
+#define CHALLENGE_MAX_ACTIVE 16
+
+static uint32_t challenge_ids[CHALLENGE_MAX_ACTIVE];
+static int challenge_count = 0;
+static int challenge_dirty = 0;
+static int last_challenge_count = 0;
+static SDL_mutex* challenge_mutex = NULL;
+
///////////////////////////////
// Rounded rectangle drawing
///////////////////////////////
@@ -170,7 +187,14 @@ void Notification_init(void) {
if (!progress_mutex) {
progress_mutex = SDL_CreateMutex();
}
-
+ // Create challenge indicator mutex
+ if (!challenge_mutex) {
+ challenge_mutex = SDL_CreateMutex();
+ }
+ challenge_count = 0;
+ challenge_dirty = 0;
+ last_challenge_count = 0;
+
render_dirty = 1;
last_notification_count = 0;
initialized = 1;
@@ -353,6 +377,70 @@ static void render_progress_indicator(const ProgressIndicatorState* snap) {
SDL_FreeSurface(progress_surface);
}
+// Render the challenge indicator: a trophy pill in the lower-right corner.
+// One trophy when a single challenge is active; trophy + "+N" when more than
+// one. `count` is the number of active challenges (>= 1).
+static void render_challenge_indicator(int count) {
+ if (count < 1) return;
+
+ SDL_Color text_color = uintToColour(THEME_COLOR1_255);
+ SDL_Color bg_color_sdl = uintToColour(THEME_COLOR2_255);
+
+ // Trophy glyph size (the asset rect is already scaled to the device scale).
+ SDL_Rect trophy_rect;
+ GFX_assetRect(ASSET_TROPHY, &trophy_rect);
+ int icon_w = trophy_rect.w;
+ int icon_h = trophy_rect.h;
+
+ // "+N" counter only when more than one challenge is active.
+ char count_text[8] = {0};
+ int text_w = 0, text_h = 0;
+ if (count > 1) {
+ snprintf(count_text, sizeof(count_text), "+%d", count - 1);
+ TTF_SizeUTF8(font.tiny, count_text, &text_w, &text_h);
+ }
+
+ int content_h = icon_h > text_h ? icon_h : text_h;
+ int gap = (count > 1) ? notif_icon_gap : 0;
+ int pill_w = icon_w + gap + text_w + (notif_padding_x * 2);
+ int pill_h = content_h + (notif_padding_y * 2);
+ int corner_radius = pill_h / 2;
+ int x = screen_width - notif_margin - pill_w;
+ int y = screen_height - notif_margin - pill_h;
+
+ SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(
+ 0, pill_w, pill_h, 32, SDL_PIXELFORMAT_ABGR8888);
+ if (!surface) return;
+
+ SDL_FillRect(surface, NULL, 0);
+ Uint32 bg_color = SDL_MapRGBA(surface->format,
+ bg_color_sdl.r, bg_color_sdl.g, bg_color_sdl.b, 255);
+ draw_rounded_rect(surface, 0, 0, pill_w, pill_h, corner_radius, bg_color);
+
+ int content_x = notif_padding_x;
+ int icon_y = notif_padding_y + (content_h - icon_h) / 2;
+ GFX_blitAssetColor(ASSET_TROPHY, NULL, surface,
+ &(SDL_Rect){content_x, icon_y}, THEME_COLOR1_255);
+ content_x += icon_w + gap;
+
+ if (count > 1) {
+ SDL_Surface* text_surf = TTF_RenderUTF8_Blended(font.tiny, count_text, text_color);
+ if (text_surf) {
+ SDL_SetSurfaceBlendMode(text_surf, SDL_BLENDMODE_BLEND);
+ SDL_Rect text_dst = {content_x,
+ notif_padding_y + (content_h - text_surf->h) / 2,
+ text_surf->w, text_surf->h};
+ SDL_BlitSurface(text_surf, NULL, surface, &text_dst);
+ SDL_FreeSurface(text_surf);
+ }
+ }
+
+ SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE);
+ SDL_Rect dst_rect = {x, y, pill_w, pill_h};
+ SDL_BlitSurface(surface, NULL, gl_notification_surface, &dst_rect);
+ SDL_FreeSurface(surface);
+}
+
// Render a single notification pill
static void render_notification_pill(Notification* n, int x, int y, SDL_Color text_color, SDL_Color bg_color_sdl) {
int text_w = 0, text_h = 0;
@@ -466,8 +554,16 @@ void Notification_renderToLayer(int layer) {
SDL_UnlockMutex(progress_mutex);
int has_progress_indicator = progress_snap.active;
-
- if (!has_notifications && !has_system_indicator && !has_progress_indicator) {
+
+ // Snapshot challenge indicator state under lock.
+ int challenge_snap = 0, challenge_snap_dirty = 0;
+ SDL_LockMutex(challenge_mutex);
+ challenge_snap = challenge_count;
+ challenge_snap_dirty = challenge_dirty;
+ SDL_UnlockMutex(challenge_mutex);
+ int has_challenge_indicator = challenge_snap > 0;
+
+ if (!has_notifications && !has_system_indicator && !has_progress_indicator && !has_challenge_indicator) {
// When all notifications and indicators are gone, render one final transparent frame
if (gl_notification_surface) {
if (needs_clear_frame) {
@@ -479,6 +575,10 @@ void Notification_renderToLayer(int layer) {
SDL_LockMutex(progress_mutex);
progress_state.dirty = 0;
SDL_UnlockMutex(progress_mutex);
+ SDL_LockMutex(challenge_mutex);
+ challenge_dirty = 0;
+ SDL_UnlockMutex(challenge_mutex);
+ last_challenge_count = 0;
last_system_indicator_type = SYSTEM_INDICATOR_NONE;
return;
}
@@ -496,8 +596,9 @@ void Notification_renderToLayer(int layer) {
int notifications_changed = render_dirty || notification_count != last_notification_count;
int indicator_changed = system_indicator_dirty || system_indicator_type != last_system_indicator_type;
int progress_changed = progress_snap.dirty;
-
- if (!notifications_changed && !indicator_changed && !progress_changed) {
+ int challenge_changed = challenge_snap_dirty || challenge_snap != last_challenge_count;
+
+ if (!notifications_changed && !indicator_changed && !progress_changed && !challenge_changed) {
return;
}
@@ -521,6 +622,9 @@ void Notification_renderToLayer(int layer) {
if (has_progress_indicator) {
render_progress_indicator(&progress_snap);
}
+ if (has_challenge_indicator) {
+ render_challenge_indicator(challenge_snap);
+ }
if (has_notifications) {
render_notification_stack();
}
@@ -538,6 +642,11 @@ void Notification_renderToLayer(int layer) {
SDL_LockMutex(progress_mutex);
progress_state.dirty = 0;
SDL_UnlockMutex(progress_mutex);
+
+ last_challenge_count = challenge_snap;
+ SDL_LockMutex(challenge_mutex);
+ challenge_dirty = 0;
+ SDL_UnlockMutex(challenge_mutex);
}
bool Notification_isActive(void) {
@@ -555,7 +664,14 @@ void Notification_clear(void) {
}
progress_state.dirty = 1;
SDL_UnlockMutex(progress_mutex);
-
+
+ if (challenge_mutex) {
+ SDL_LockMutex(challenge_mutex);
+ challenge_count = 0;
+ challenge_dirty = 1;
+ SDL_UnlockMutex(challenge_mutex);
+ }
+
render_dirty = 1;
PLAT_clearNotificationSurface();
if (gl_notification_surface) {
@@ -568,11 +684,15 @@ void Notification_quit(void) {
Notification_clear();
system_indicator_type = SYSTEM_INDICATOR_NONE;
initialized = 0;
-
+
if (progress_mutex) {
SDL_DestroyMutex(progress_mutex);
progress_mutex = NULL;
}
+ if (challenge_mutex) {
+ SDL_DestroyMutex(challenge_mutex);
+ challenge_mutex = NULL;
+ }
}
///////////////////////////////
@@ -666,3 +786,56 @@ bool Notification_hasProgressIndicator(void) {
SDL_UnlockMutex(progress_mutex);
return active;
}
+
+///////////////////////////////
+// Challenge Indicator Functions
+///////////////////////////////
+
+void Notification_showChallengeIndicator(uint32_t ach_id) {
+ if (!initialized) return;
+ SDL_LockMutex(challenge_mutex);
+ int found = 0;
+ for (int i = 0; i < challenge_count; i++) {
+ if (challenge_ids[i] == ach_id) { found = 1; break; }
+ }
+ if (!found && challenge_count < CHALLENGE_MAX_ACTIVE) {
+ challenge_ids[challenge_count++] = ach_id;
+ challenge_dirty = 1;
+ }
+ SDL_UnlockMutex(challenge_mutex);
+}
+
+void Notification_hideChallengeIndicator(uint32_t ach_id) {
+ if (!initialized) return;
+ SDL_LockMutex(challenge_mutex);
+ for (int i = 0; i < challenge_count; i++) {
+ if (challenge_ids[i] == ach_id) {
+ // Remove by shifting the tail down.
+ for (int j = i; j < challenge_count - 1; j++) {
+ challenge_ids[j] = challenge_ids[j + 1];
+ }
+ challenge_count--;
+ challenge_dirty = 1;
+ break;
+ }
+ }
+ SDL_UnlockMutex(challenge_mutex);
+}
+
+void Notification_clearChallengeIndicators(void) {
+ if (!challenge_mutex) return;
+ SDL_LockMutex(challenge_mutex);
+ if (challenge_count > 0) {
+ challenge_count = 0;
+ challenge_dirty = 1;
+ }
+ SDL_UnlockMutex(challenge_mutex);
+}
+
+bool Notification_hasChallengeIndicator(void) {
+ if (!initialized) return false;
+ SDL_LockMutex(challenge_mutex);
+ bool active = challenge_count > 0;
+ SDL_UnlockMutex(challenge_mutex);
+ return active;
+}
diff --git a/workspace/all/common/notification.h b/workspace/all/common/notification.h
index eeb3608fc..3ce8f9a78 100644
--- a/workspace/all/common/notification.h
+++ b/workspace/all/common/notification.h
@@ -157,4 +157,38 @@ void Notification_setProgressIndicatorPersistent(bool persistent);
*/
bool Notification_hasProgressIndicator(void);
+///////////////////////////////
+// Challenge Indicators
+// Shows a persistent trophy pill in the lower-right while one or more
+// "challenge" achievements are active (e.g. a no-damage run in progress).
+// Tracked by achievement id so duplicate SHOW or out-of-order HIDE events
+// are idempotent. A single trophy is shown for one active challenge, with a
+// "+N" counter when more than one is active.
+///////////////////////////////
+
+/**
+ * Mark a challenge achievement as active (RC_CLIENT_EVENT_ACHIEVEMENT_
+ * CHALLENGE_INDICATOR_SHOW). Idempotent for a given id.
+ * @param ach_id The rcheevos achievement id
+ */
+void Notification_showChallengeIndicator(uint32_t ach_id);
+
+/**
+ * Mark a challenge achievement as no longer active (CHALLENGE_INDICATOR_HIDE).
+ * Idempotent — a HIDE for an unknown id is a no-op.
+ * @param ach_id The rcheevos achievement id
+ */
+void Notification_hideChallengeIndicator(uint32_t ach_id);
+
+/**
+ * Clear all active challenge indicators (e.g. on game unload).
+ */
+void Notification_clearChallengeIndicators(void);
+
+/**
+ * Check if any challenge indicator is currently active.
+ * @return true if at least one challenge is active
+ */
+bool Notification_hasChallengeIndicator(void);
+
#endif // __NOTIFICATION_H__
diff --git a/workspace/all/minarch/ma_menu.c b/workspace/all/minarch/ma_menu.c
index 5a5c130d0..85ca4341a 100644
--- a/workspace/all/minarch/ma_menu.c
+++ b/workspace/all/minarch/ma_menu.c
@@ -544,9 +544,10 @@ static int OptionAchievements_openMenu(MenuList* list, int i) {
}
ach_menu_count = 0;
- // Create achievement list grouped by lock state
+ // Group by progress so rcheevos surfaces a separate ACTIVE_CHALLENGE bucket
+ // (achievements whose trigger is currently primed). We pin those to the top.
ach_menu_list = (const rc_client_achievement_list_t*)RA_createAchievementList(
- RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE);
+ RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);
if (!ach_menu_list) {
Menu_message("Failed to load achievements", (char*[]){"B","BACK", NULL});
@@ -568,18 +569,37 @@ static int OptionAchievements_openMenu(MenuList* list, int i) {
return MENU_CALLBACK_NOP;
}
- // Create flattened array of all achievement pointers
+ // Create flattened array of all achievement pointers.
+ // Two passes so active challenges stay pinned at the top:
+ // 1. ACTIVE_CHALLENGE bucket entries, in rcheevos' natural order (no sort)
+ // 2. everything else
+ // Only the second pass is sorted by the user's chosen order, so the pinned
+ // challenges remain at the top regardless of sort.
const rc_client_achievement_t** all_achievements = calloc(total_achievements, sizeof(rc_client_achievement_t*));
int idx = 0;
+
+ // Pass 1: pinned active challenges
+ for (uint32_t b = 0; b < ach_menu_list->num_buckets && idx < total_achievements; b++) {
+ const rc_client_achievement_bucket_t* bucket = &ach_menu_list->buckets[b];
+ if (bucket->bucket_type != RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE) continue;
+ for (uint32_t a = 0; a < bucket->num_achievements && idx < total_achievements; a++) {
+ all_achievements[idx++] = bucket->achievements[a];
+ }
+ }
+ int challenge_pin_count = idx;
+
+ // Pass 2: all other buckets
for (uint32_t b = 0; b < ach_menu_list->num_buckets && idx < total_achievements; b++) {
const rc_client_achievement_bucket_t* bucket = &ach_menu_list->buckets[b];
+ if (bucket->bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE) continue;
for (uint32_t a = 0; a < bucket->num_achievements && idx < total_achievements; a++) {
all_achievements[idx++] = bucket->achievements[a];
}
}
- // Sort achievements according to settings
- ach_sort_achievements(all_achievements, total_achievements);
+ // Sort only the non-pinned tail according to settings
+ ach_sort_achievements(all_achievements + challenge_pin_count,
+ total_achievements - challenge_pin_count);
// Custom menu loop with X (mute) and Y (filter) support
int dirty = 1;
@@ -752,6 +772,7 @@ static int OptionAchievements_openMenu(MenuList* list, int i) {
const rc_client_achievement_t* ach = filtered[j];
bool is_muted = RA_isAchievementMuted(ach->id);
bool is_offline_pending = RA_isAchievementOfflinePending(ach->id);
+ bool is_challenge = ach->bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE;
bool is_selected = (row == selected_row);
SDL_Color text_color = COLOR_WHITE;
@@ -784,7 +805,11 @@ static int OptionAchievements_openMenu(MenuList* list, int i) {
if (is_offline_pending) {
offline_width = SCALE1(12) + SCALE1(4); // wifi-off icon + gap
}
- int pill_width = opt_pad + badge_display_size + SCALE1(6) + mute_width + offline_width + title_width + opt_pad;
+ int challenge_width = 0;
+ if (is_challenge) {
+ challenge_width = SCALE1(10) + SCALE1(4); // trophy icon + gap
+ }
+ int pill_width = opt_pad + badge_display_size + SCALE1(6) + mute_width + offline_width + challenge_width + title_width + opt_pad;
GFX_blitPillDark(ASSET_BUTTON, screen, &(SDL_Rect){
ox, oy + SCALE1(row * BUTTON_SIZE), pill_width, row_height
@@ -824,6 +849,15 @@ static int OptionAchievements_openMenu(MenuList* list, int i) {
text_x += offline_width;
}
+ // Active-challenge trophy prefix
+ if (is_challenge) {
+ int trophy_h = SCALE1(11);
+ int trophy_y = oy + SCALE1(row * BUTTON_SIZE) + (row_height - trophy_h) / 2;
+ GFX_blitAssetColor(ASSET_TROPHY, NULL, screen,
+ &(SDL_Rect){text_x, trophy_y}, THEME_COLOR5_255);
+ text_x += challenge_width;
+ }
+
// Title text
SDL_Surface* title_text = TTF_RenderUTF8_Blended(font.small, ach->title, text_color);
SDL_BlitSurface(title_text, NULL, screen, &(SDL_Rect){
@@ -868,6 +902,15 @@ static int OptionAchievements_openMenu(MenuList* list, int i) {
text_x += wifi_size + SCALE1(4);
}
+ // Active-challenge trophy prefix
+ if (is_challenge) {
+ int trophy_h = SCALE1(11);
+ int trophy_y = oy + SCALE1(row * BUTTON_SIZE) + (row_height - trophy_h) / 2;
+ GFX_blitAssetColor(ASSET_TROPHY, NULL, screen,
+ &(SDL_Rect){text_x, trophy_y}, RGB_WHITE);
+ text_x += SCALE1(10) + SCALE1(4);
+ }
+
// Title text (white for unselected)
SDL_Surface* title_text = TTF_RenderUTF8_Blended(font.small, ach->title, COLOR_WHITE);
SDL_BlitSurface(title_text, NULL, screen, &(SDL_Rect){
diff --git a/workspace/all/minarch/ra_integration.c b/workspace/all/minarch/ra_integration.c
index 7347e3388..ec64ab66e 100644
--- a/workspace/all/minarch/ra_integration.c
+++ b/workspace/all/minarch/ra_integration.c
@@ -1317,10 +1317,16 @@ static void ra_event_handler(const rc_client_event_t* event, rc_client_t* client
break;
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW:
+ if (CFG_getRAShowChallengeIndicators()) {
+ Notification_showChallengeIndicator(event->achievement->id);
+ }
RA_LOG_DEBUG("Challenge started: %s\n", event->achievement->title);
break;
-
+
case RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE:
+ // Always hide (even if indicators were toggled off mid-challenge) so a
+ // stale indicator can't get stuck on screen.
+ Notification_hideChallengeIndicator(event->achievement->id);
RA_LOG_DEBUG("Challenge ended: %s\n", event->achievement->title);
break;
diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp
index 7a3eeda49..dccdc9bf3 100644
--- a/workspace/all/settings/settings.cpp
+++ b/workspace/all/settings/settings.cpp
@@ -882,6 +882,10 @@ int main(int argc, char *argv[])
[]() -> std::any { return CFG_getRAShowNotifications(); },
[](const std::any &value) { CFG_setRAShowNotifications(std::any_cast(value)); },
[]() { CFG_setRAShowNotifications(CFG_DEFAULT_RA_SHOW_NOTIFICATIONS);}},
+ new MenuItem{ListItemType::Generic, "Show Challenge Indicators", "Show a trophy indicator while a challenge achievement is active", {false, true}, on_off,
+ []() -> std::any { return CFG_getRAShowChallengeIndicators(); },
+ [](const std::any &value) { CFG_setRAShowChallengeIndicators(std::any_cast(value)); },
+ []() { CFG_setRAShowChallengeIndicators(CFG_DEFAULT_RA_SHOW_CHALLENGE_INDICATORS);}},
new MenuItem{ListItemType::Generic, "Notification Duration", "How long achievement notifications stay on screen", notify_duration_values, notify_duration_labels,
[]() -> std::any { return CFG_getRANotificationDuration(); },
[](const std::any &value) { CFG_setRANotificationDuration(std::any_cast(value)); },