Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion skeleton/SYSTEM/res/assets.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified skeleton/SYSTEM/res/assets@1x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified skeleton/SYSTEM/res/assets@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified skeleton/SYSTEM/res/assets@3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified skeleton/SYSTEM/res/assets@4x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions workspace/all/common/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)};
Expand Down
1 change: 1 addition & 0 deletions workspace/all/common/api.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ enum {

ASSET_CHECKCIRCLE,
ASSET_LOCK,
ASSET_TROPHY,
ASSET_SETTINGS,
ASSET_STORE,
ASSET_GAMEPAD,
Expand Down
23 changes: 23 additions & 0 deletions workspace/all/common/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions workspace/all/common/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
187 changes: 180 additions & 7 deletions workspace/all/common/notification.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
///////////////////////////////
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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();
}
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
}
}

///////////////////////////////
Expand Down Expand Up @@ -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;
}
Loading
Loading