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)); },