Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
fef0be5
Fix badge download cap at 256 preventing large achievement sets from …
clintonium-119 Mar 23, 2026
c7013fa
Add offline achievements development plan
clintonium-119 Mar 23, 2026
8651004
Update dev working file
clintonium-119 Mar 23, 2026
4ced72f
WIP: Initial working-ish commit
clintonium-119 Mar 23, 2026
9cb0e37
WIP: Sync generally working, but still some issues with a potentially…
clintonium-119 Mar 24, 2026
f950873
Fix order-dependent SYNC_ACK matching and production hardening for of…
clintonium-119 Mar 24, 2026
4f03cac
Add connectivity state machine for offline-first RA startup (Phase 7)
clintonium-119 Mar 24, 2026
40ae83f
Fix offline-to-online transition reliability and reduce notification …
clintonium-119 Mar 24, 2026
515a994
Clean up RA integration: reduce log noise, remove dead variables, add…
clintonium-119 Mar 24, 2026
b28a8df
Fix notification thread safety and system indicator width on Brick
clintonium-119 Mar 24, 2026
ce3992b
Remove planning doc
clintonium-119 Mar 24, 2026
19d9387
Merge branch 'main' into offline-achievements
clintonium-119 Mar 24, 2026
080ebd5
Fix RA offline thread safety: add ledger mutex, reorder shutdown, val…
clintonium-119 Mar 25, 2026
cb65800
Remove RA offline/connected status notifications
clintonium-119 Mar 25, 2026
a66fa92
Fix offline achievement sync not updating count or list until second …
clintonium-119 Mar 25, 2026
4f18d06
Refactor ra_offline.c: extract SHA-256, fix hardcore filter, deduplic…
clintonium-119 Mar 25, 2026
a751adf
Fix race: defer offline sync until game is loaded
clintonium-119 Mar 26, 2026
1102324
Fix online unlocks showing as offline-pending after mid-game sync
clintonium-119 Mar 26, 2026
ce417bf
Add RA offline sync engine and Settings sync button
clintonium-119 Mar 26, 2026
debade3
Fix stale startsession cache after online achievement unlock sync
clintonium-119 Mar 26, 2026
9283441
Show pending offline unlock count in Settings sync button description
clintonium-119 Mar 26, 2026
697fcd7
fix: patch startsession cache in-place instead of deleting it on onli…
clintonium-119 Mar 26, 2026
b831f92
perf(ra): fix main-thread stalls on achievement unlock
clintonium-119 Mar 26, 2026
df716d9
Replace [O] text tag with wifi-off icon for offline achievement indic…
clintonium-119 Mar 26, 2026
c5ad104
feat(ra): replace text indicators with sprite icons in achievement UI
clintonium-119 Mar 26, 2026
faeb30e
Fix sync notification showing total pending count instead of game-fil…
clintonium-119 Mar 30, 2026
b487181
WIP: fix offline achievement timestamp bugs and add diagnostic logging
clintonium-119 Mar 31, 2026
b067771
Show notification when game is not recognized by RetroAchievements
clintonium-119 Apr 1, 2026
6254f8c
fix(ra): prevent timestamp loss in offline→online achievement sync
clintonium-119 Apr 2, 2026
9b8aeeb
fix(ra): block rcheevos retry path to prevent duplicate awardachievem…
clintonium-119 Apr 3, 2026
d55fec9
fix(ra): retry game load on connectivity restore when offline cache m…
clintonium-119 Apr 5, 2026
415dc40
Merge branch main into offline-achievements
clintonium-119 Apr 8, 2026
483c358
Fix offline achievement unlock times for renamed RA accounts
clintonium-119 Apr 9, 2026
0c64b58
fix: resolve offline achievement timestamp bug caused by RA username …
clintonium-119 Apr 9, 2026
c6a5b1f
Small text tweak
clintonium-119 Apr 9, 2026
1777df2
refactor: harden and restructure offline RetroAchievements subsystem
clintonium-119 Apr 10, 2026
5e7dbb4
fix(ra): detect WiFi drops mid-game and sync offline achievements on …
clintonium-119 Apr 10, 2026
5d83814
refactor(ra): replace volatile bools with SDL_AtomicInt, rename ra_fs…
clintonium-119 Apr 10, 2026
9003231
fix(ra): clear pending cache after sync-apply, fix notification TOCTO…
clintonium-119 Apr 13, 2026
7ad6391
fix(ra): clear stale raServerUsername when AvatarUrl is unavailable
clintonium-119 Apr 15, 2026
d2160b3
refactor(ra): pin raServerUsername to settings-auth, drop background …
clintonium-119 Apr 15, 2026
5e16198
fix(ra): match JSON-escaped "\/UserPic\/" in AvatarUrl parser
clintonium-119 Apr 15, 2026
bfb5015
core: clean up comment in parse_login_response
clintonium-119 Apr 22, 2026
2e56cee
chore: clean up MINARCH_BUILD_VERSION debugging
clintonium-119 Apr 22, 2026
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
50 changes: 50 additions & 0 deletions workspace/all/common/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ void CFG_defaults(NextUISettings *cfg)
.raPassword = CFG_DEFAULT_RA_PASSWORD,
.raHardcoreMode = CFG_DEFAULT_RA_HARDCOREMODE,
.raToken = CFG_DEFAULT_RA_TOKEN,
.raServerUsername = CFG_DEFAULT_RA_SERVER_USERNAME,
.raAuthenticated = CFG_DEFAULT_RA_AUTHENTICATED,
.raShowNotifications = CFG_DEFAULT_RA_SHOW_NOTIFICATIONS,
.raNotificationDuration = CFG_DEFAULT_RA_NOTIFICATION_DURATION,
Expand Down Expand Up @@ -371,6 +372,13 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
CFG_setRAToken(value);
continue;
}
if (strncmp(line, "raServerUsername=", 17) == 0)
{
char *value = line + 17;
value[strcspn(value, "\n")] = 0;
CFG_setRAServerUsername(value);
continue;
}
if (sscanf(line, "raAuthenticated=%i", &temp_value) == 1)
{
CFG_setRAAuthenticated((bool)temp_value);
Expand Down Expand Up @@ -961,6 +969,47 @@ void CFG_setRAToken(const char* token)
CFG_sync();
}

const char* CFG_getRAServerUsername(void)
{
return settings.raServerUsername;
}

void CFG_setRAServerUsername(const char* username)
{
if (username) {
strncpy(settings.raServerUsername, username, sizeof(settings.raServerUsername) - 1);
settings.raServerUsername[sizeof(settings.raServerUsername) - 1] = '\0';
} else {
settings.raServerUsername[0] = '\0';
}
CFG_sync();
}

bool CFG_setRAServerUsernameFromAvatarUrl(const char* str)
{
if (!str) return false;
/* Accept both unescaped "/UserPic/" (rcheevos-decoded strings) and
* JSON-escaped "\/UserPic\/" (raw RA API response bodies). */
const char* marker = strstr(str, "/UserPic/");
size_t marker_skip = 9; /* strlen("/UserPic/") */
if (!marker) {
marker = strstr(str, "\\/UserPic\\/");
marker_skip = 11; /* strlen("\\/UserPic\\/") */
if (!marker) return false;
}
marker += marker_skip;
/* Name ends at ".png" (or its escaped form, though ".png" has no slash). */
const char* dot = strstr(marker, ".png");
if (!dot || dot <= marker) return false;
size_t len = (size_t)(dot - marker);
if (len == 0 || len >= sizeof(settings.raServerUsername)) return false;
char username[64];
memcpy(username, marker, len);
username[len] = '\0';
CFG_setRAServerUsername(username);
return true;
}

bool CFG_getRAAuthenticated(void)
{
return settings.raAuthenticated;
Expand Down Expand Up @@ -1253,6 +1302,7 @@ void CFG_sync(void)
fprintf(file, "raPassword=%s\n", settings.raPassword);
fprintf(file, "raHardcoreMode=%i\n", settings.raHardcoreMode);
fprintf(file, "raToken=%s\n", settings.raToken);
fprintf(file, "raServerUsername=%s\n", settings.raServerUsername);
fprintf(file, "raAuthenticated=%i\n", settings.raAuthenticated);
fprintf(file, "raShowNotifications=%i\n", settings.raShowNotifications);
fprintf(file, "raNotificationDuration=%i\n", settings.raNotificationDuration);
Expand Down
10 changes: 10 additions & 0 deletions workspace/all/common/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ typedef struct
char raPassword[128];
bool raHardcoreMode;
char raToken[64]; // API token (stored after successful auth)
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
int raNotificationDuration; // Duration for achievement notifications (1-5 seconds)
Expand Down Expand Up @@ -224,6 +225,7 @@ typedef struct
#define CFG_DEFAULT_RA_PASSWORD ""
#define CFG_DEFAULT_RA_HARDCOREMODE false
#define CFG_DEFAULT_RA_TOKEN ""
#define CFG_DEFAULT_RA_SERVER_USERNAME ""
#define CFG_DEFAULT_RA_AUTHENTICATED false
#define CFG_DEFAULT_RA_SHOW_NOTIFICATIONS true
#define CFG_DEFAULT_RA_NOTIFICATION_DURATION 3
Expand Down Expand Up @@ -368,6 +370,14 @@ bool CFG_getRAHardcoreMode(void);
void CFG_setRAHardcoreMode(bool enable);
const char* CFG_getRAToken(void);
void CFG_setRAToken(const char* token);
const char* CFG_getRAServerUsername(void);
void CFG_setRAServerUsername(const char* username);
// Extract the RA server's internal username from any string containing
// "/UserPic/USERNAME.png" (e.g. an avatar URL or raw JSON) and persist
// it via CFG_setRAServerUsername(). Returns true if a username was
// extracted and stored; returns false (and leaves any existing stored
// value untouched) if the pattern is missing or malformed.
bool CFG_setRAServerUsernameFromAvatarUrl(const char* str);
bool CFG_getRAAuthenticated(void);
void CFG_setRAAuthenticated(bool authenticated);
bool CFG_getRAShowNotifications(void);
Expand Down
172 changes: 172 additions & 0 deletions workspace/all/common/md5.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* MD5 hash implementation for NextUI.
* Based on RFC 1321. Public domain.
* Same API pattern as sha256.h/sha256.c in this project.
*
* All symbols prefixed with nui_md5_ to avoid collision with
* rcheevos' internal rhash/md5 when linked with LTO.
*/

#include "md5.h"
#include <string.h>
#include <stdio.h>

/* MD5 round constants */
static const uint32_t K[64] = {
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
};

/* Per-round shift amounts */
static const uint8_t S[64] = {
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
};

#define ROTLEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))

static void nui_md5_transform(NUI_MD5_CTX* ctx, const uint8_t block[NUI_MD5_BLOCK_SIZE]) {
uint32_t M[16];
uint32_t a, b, c, d, f, g, temp;

/* Decode block into 16 little-endian 32-bit words */
for (int i = 0; i < 16; i++) {
M[i] = (uint32_t)block[i * 4]
| ((uint32_t)block[i * 4 + 1] << 8)
| ((uint32_t)block[i * 4 + 2] << 16)
| ((uint32_t)block[i * 4 + 3] << 24);
}

a = ctx->state[0];
b = ctx->state[1];
c = ctx->state[2];
d = ctx->state[3];

for (int i = 0; i < 64; i++) {
if (i < 16) {
f = (b & c) | (~b & d);
g = i;
} else if (i < 32) {
f = (d & b) | (~d & c);
g = (5 * i + 1) % 16;
} else if (i < 48) {
f = b ^ c ^ d;
g = (3 * i + 5) % 16;
} else {
f = c ^ (b | ~d);
g = (7 * i) % 16;
}

temp = d;
d = c;
c = b;
b = b + ROTLEFT((a + f + K[i] + M[g]), S[i]);
a = temp;
}

ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
}

void nui_md5_init(NUI_MD5_CTX* ctx) {
ctx->count = 0;
ctx->state[0] = 0x67452301;
ctx->state[1] = 0xefcdab89;
ctx->state[2] = 0x98badcfe;
ctx->state[3] = 0x10325476;
}

void nui_md5_update(NUI_MD5_CTX* ctx, const void* data, size_t len) {
const uint8_t* p = (const uint8_t*)data;
size_t offset = (size_t)(ctx->count % NUI_MD5_BLOCK_SIZE);
ctx->count += len;

/* Fill partial block */
if (offset > 0) {
size_t avail = NUI_MD5_BLOCK_SIZE - offset;
if (len < avail) {
memcpy(ctx->buffer + offset, p, len);
return;
}
memcpy(ctx->buffer + offset, p, avail);
nui_md5_transform(ctx, ctx->buffer);
p += avail;
len -= avail;
}

/* Process full blocks */
while (len >= NUI_MD5_BLOCK_SIZE) {
nui_md5_transform(ctx, p);
p += NUI_MD5_BLOCK_SIZE;
len -= NUI_MD5_BLOCK_SIZE;
}

/* Buffer remainder */
if (len > 0) {
memcpy(ctx->buffer, p, len);
}
}

void nui_md5_final(NUI_MD5_CTX* ctx, uint8_t digest[NUI_MD5_DIGEST_SIZE]) {
uint64_t bits = ctx->count * 8;
size_t offset = (size_t)(ctx->count % NUI_MD5_BLOCK_SIZE);

/* Pad with 0x80 then zeros */
ctx->buffer[offset++] = 0x80;

if (offset > 56) {
/* Not enough room for length; pad to end and process */
memset(ctx->buffer + offset, 0, NUI_MD5_BLOCK_SIZE - offset);
nui_md5_transform(ctx, ctx->buffer);
offset = 0;
}
memset(ctx->buffer + offset, 0, 56 - offset);

/* Append length in bits as 64-bit little-endian */
for (int i = 0; i < 8; i++) {
ctx->buffer[56 + i] = (uint8_t)(bits >> (i * 8));
}
nui_md5_transform(ctx, ctx->buffer);

/* Encode state as little-endian bytes */
for (int i = 0; i < 4; i++) {
digest[i * 4] = (uint8_t)(ctx->state[i]);
digest[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 8);
digest[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 16);
digest[i * 4 + 3] = (uint8_t)(ctx->state[i] >> 24);
}
}

void nui_md5_hash(const void* data, size_t len, uint8_t digest[NUI_MD5_DIGEST_SIZE]) {
NUI_MD5_CTX ctx;
nui_md5_init(&ctx);
nui_md5_update(&ctx, data, len);
nui_md5_final(&ctx, digest);
}

void nui_md5_hash_hex(const void* data, size_t len, char hex_out[33]) {
uint8_t digest[NUI_MD5_DIGEST_SIZE];
nui_md5_hash(data, len, digest);
for (int i = 0; i < NUI_MD5_DIGEST_SIZE; i++) {
sprintf(hex_out + i * 2, "%02x", digest[i]);
}
hex_out[32] = '\0';
}
40 changes: 40 additions & 0 deletions workspace/all/common/md5.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#ifndef NUI_MD5_H
#define NUI_MD5_H

#include <stddef.h>
#include <stdint.h>

/*
* Standalone MD5 implementation for NextUI.
* All symbols prefixed with nui_md5_ to avoid collision with
* rcheevos' internal rhash/md5 when linked with LTO.
*/

#define NUI_MD5_BLOCK_SIZE 64
#define NUI_MD5_DIGEST_SIZE 16

typedef struct {
uint32_t state[4];
uint64_t count;
uint8_t buffer[NUI_MD5_BLOCK_SIZE];
} NUI_MD5_CTX;

#ifdef __cplusplus
extern "C" {
#endif

void nui_md5_init(NUI_MD5_CTX* ctx);
void nui_md5_update(NUI_MD5_CTX* ctx, const void* data, size_t len);
void nui_md5_final(NUI_MD5_CTX* ctx, uint8_t digest[NUI_MD5_DIGEST_SIZE]);

/* Convenience: hash a buffer in one shot */
void nui_md5_hash(const void* data, size_t len, uint8_t digest[NUI_MD5_DIGEST_SIZE]);

/* Convenience: hash a buffer and write hex string (32 chars + null) */
void nui_md5_hash_hex(const void* data, size_t len, char hex_out[33]);

#ifdef __cplusplus
}
#endif

#endif /* NUI_MD5_H */
Loading
Loading