duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

rc_client.c (214486B)


      1 #include "rc_client_internal.h"
      2 
      3 #include "rc_api_info.h"
      4 #include "rc_api_runtime.h"
      5 #include "rc_api_user.h"
      6 #include "rc_consoles.h"
      7 #include "rc_hash.h"
      8 #include "rc_version.h"
      9 
     10 #include "rapi/rc_api_common.h"
     11 
     12 #include "rcheevos/rc_internal.h"
     13 
     14 #include <stdarg.h>
     15 
     16 #ifdef _WIN32
     17 #define WIN32_LEAN_AND_MEAN
     18 #include <windows.h>
     19 #include <profileapi.h>
     20 #else
     21 #include <time.h>
     22 #endif
     23 
     24 #define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1
     25 #define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */
     26 
     27 #define RC_MINIMUM_UNPAUSED_FRAMES 20
     28 #define RC_PAUSE_DECAY_MULTIPLIER 4
     29 
     30 enum {
     31   RC_CLIENT_ASYNC_NOT_ABORTED = 0,
     32   RC_CLIENT_ASYNC_ABORTED = 1,
     33   RC_CLIENT_ASYNC_DESTROYED = 2
     34 };
     35 
     36 typedef struct rc_client_generic_callback_data_t {
     37   rc_client_t* client;
     38   rc_client_callback_t callback;
     39   void* callback_userdata;
     40   rc_client_async_handle_t async_handle;
     41 } rc_client_generic_callback_data_t;
     42 
     43 typedef struct rc_client_pending_media_t
     44 {
     45 #ifdef RC_CLIENT_SUPPORTS_HASH
     46   const char* file_path;
     47   uint8_t* data;
     48   size_t data_size;
     49 #endif
     50   const char* hash;
     51   rc_client_callback_t callback;
     52   void* callback_userdata;
     53 } rc_client_pending_media_t;
     54 
     55 typedef struct rc_client_load_state_t
     56 {
     57   rc_client_t* client;
     58   rc_client_callback_t callback;
     59   void* callback_userdata;
     60 
     61   rc_client_game_info_t* game;
     62   rc_client_subset_info_t* subset;
     63   rc_client_game_hash_t* hash;
     64 
     65 #ifdef RC_CLIENT_SUPPORTS_HASH
     66   rc_hash_iterator_t hash_iterator;
     67 #endif
     68   rc_client_pending_media_t* pending_media;
     69 
     70   rc_api_start_session_response_t *start_session_response;
     71 
     72   rc_client_async_handle_t async_handle;
     73 
     74   uint8_t progress;
     75   uint8_t outstanding_requests;
     76 #ifdef RC_CLIENT_SUPPORTS_HASH
     77   uint8_t hash_console_id;
     78 #endif
     79 } rc_client_load_state_t;
     80 
     81 static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state);
     82 static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data);
     83 static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game);
     84 static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message);
     85 static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path);
     86 static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
     87 static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset);
     88 static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game);
     89 static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when);
     90 static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
     91 static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id);
     92 static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
     93 
     94 /* ===== Construction/Destruction ===== */
     95 
     96 static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client)
     97 {
     98   (void)event;
     99   (void)client;
    100 }
    101 
    102 rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function)
    103 {
    104   rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t));
    105   if (!client)
    106     return NULL;
    107 
    108   client->state.hardcore = 1;
    109   client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES;
    110 
    111   client->callbacks.read_memory = read_memory_function;
    112   client->callbacks.server_call = server_call_function;
    113   client->callbacks.event_handler = rc_client_dummy_event_handler;
    114   rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO);
    115   rc_client_set_get_time_millisecs_function(client, NULL);
    116 
    117   rc_mutex_init(&client->state.mutex);
    118 
    119   rc_buffer_init(&client->state.buffer);
    120 
    121   return client;
    122 }
    123 
    124 void rc_client_destroy(rc_client_t* client)
    125 {
    126   if (!client)
    127     return;
    128 
    129   rc_mutex_lock(&client->state.mutex);
    130   {
    131     size_t i;
    132     for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
    133       if (client->state.async_handles[i])
    134         client->state.async_handles[i]->aborted = RC_CLIENT_ASYNC_DESTROYED;
    135     }
    136 
    137     if (client->state.load) {
    138       client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_DESTROYED;
    139       client->state.load = NULL;
    140     }
    141   }
    142   rc_mutex_unlock(&client->state.mutex);
    143 
    144   rc_client_unload_game(client);
    145 
    146 #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION
    147   rc_client_unload_raintegration(client);
    148 #endif
    149 
    150 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    151   if (client->state.external_client && client->state.external_client->destroy)
    152     client->state.external_client->destroy();
    153 #endif
    154 
    155   rc_buffer_destroy(&client->state.buffer);
    156 
    157   rc_mutex_destroy(&client->state.mutex);
    158 
    159   free(client);
    160 }
    161 
    162 /* ===== Logging ===== */
    163 
    164 static rc_client_t* g_hash_client = NULL;
    165 
    166 #ifdef RC_CLIENT_SUPPORTS_HASH
    167 static void rc_client_log_hash_message(const char* message) {
    168   rc_client_log_message(g_hash_client, message);
    169 }
    170 #endif
    171 
    172 void rc_client_log_message(const rc_client_t* client, const char* message)
    173 {
    174   if (client->callbacks.log_call)
    175     client->callbacks.log_call(message, client);
    176 }
    177 
    178 static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args)
    179 {
    180   if (client->callbacks.log_call) {
    181     char buffer[2048];
    182 
    183 #ifdef __STDC_WANT_SECURE_LIB__
    184     vsprintf_s(buffer, sizeof(buffer), format, args);
    185 #elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */
    186     vsnprintf(buffer, sizeof(buffer), format, args);
    187 #else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */
    188     vsprintf(buffer, format, args);
    189 #endif
    190 
    191     client->callbacks.log_call(buffer, client);
    192   }
    193 }
    194 
    195 #ifdef RC_NO_VARIADIC_MACROS
    196 
    197 void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...)
    198 {
    199   if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) {
    200     va_list args;
    201     va_start(args, format);
    202     rc_client_log_message_va(client, format, args);
    203     va_end(args);
    204   }
    205 }
    206 
    207 void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...)
    208 {
    209   if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) {
    210     va_list args;
    211     va_start(args, format);
    212     rc_client_log_message_va(client, format, args);
    213     va_end(args);
    214   }
    215 }
    216 
    217 void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...)
    218 {
    219   if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) {
    220     va_list args;
    221     va_start(args, format);
    222     rc_client_log_message_va(client, format, args);
    223     va_end(args);
    224   }
    225 }
    226 
    227 void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...)
    228 {
    229   if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) {
    230     va_list args;
    231     va_start(args, format);
    232     rc_client_log_message_va(client, format, args);
    233     va_end(args);
    234   }
    235 }
    236 
    237 #else
    238 
    239 void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...)
    240 {
    241   va_list args;
    242   va_start(args, format);
    243   rc_client_log_message_va(client, format, args);
    244   va_end(args);
    245 }
    246 
    247 #endif /* RC_NO_VARIADIC_MACROS */
    248 
    249 void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback)
    250 {
    251   client->callbacks.log_call = callback;
    252   client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE;
    253 
    254 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    255   if (client->state.external_client && client->state.external_client->enable_logging)
    256     client->state.external_client->enable_logging(client, level, callback);
    257 #endif
    258 }
    259 
    260 /* ===== Common ===== */
    261 
    262 static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client)
    263 {
    264 #if defined(CLOCK_MONOTONIC)
    265   struct timespec now;
    266   (void)client;
    267 
    268   if (clock_gettime(CLOCK_MONOTONIC, &now) < 0)
    269     return 0;
    270 
    271   /* round nanoseconds to nearest millisecond and add to seconds */
    272   return ((rc_clock_t)now.tv_sec * 1000 + ((rc_clock_t)now.tv_nsec / 1000000));
    273 #elif defined(_WIN32)
    274   static LARGE_INTEGER freq;
    275   LARGE_INTEGER ticks;
    276 
    277   (void)client;
    278 
    279   /* Frequency is the number of ticks per second and is guaranteed to not change. */
    280   if (!freq.QuadPart) {
    281     if (!QueryPerformanceFrequency(&freq))
    282       return 0;
    283 
    284     /* convert to number of ticks per millisecond to simplify later calculations */
    285     freq.QuadPart /= 1000;
    286   }
    287 
    288   if (!QueryPerformanceCounter(&ticks))
    289     return 0;
    290 
    291   return (rc_clock_t)(ticks.QuadPart / freq.QuadPart);
    292 #else
    293   const clock_t clock_now = clock();
    294 
    295   (void)client;
    296 
    297   if (sizeof(clock_t) == 4) {
    298     static uint32_t clock_wraps = 0;
    299     static clock_t last_clock = 0;
    300     static time_t last_timet = 0;
    301     const time_t time_now = time(NULL);
    302 
    303     if (last_timet != 0) {
    304       const time_t seconds_per_clock_t = (time_t)(((uint64_t)1 << 32) / CLOCKS_PER_SEC);
    305       if (clock_now < last_clock) {
    306         /* clock() has wrapped */
    307         ++clock_wraps;
    308       }
    309       else if (time_now - last_timet > seconds_per_clock_t) {
    310         /* it's been long enough that clock() has wrapped and is higher than the last time it was read */
    311         ++clock_wraps;
    312       }
    313     }
    314 
    315     last_timet = time_now;
    316     last_clock = clock_now;
    317 
    318     return (rc_clock_t)((((uint64_t)clock_wraps << 32) | clock_now) / (CLOCKS_PER_SEC / 1000));
    319   }
    320   else {
    321     return (rc_clock_t)(clock_now / (CLOCKS_PER_SEC / 1000));
    322   }
    323 #endif
    324 }
    325 
    326 void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler)
    327 {
    328   client->callbacks.get_time_millisecs = handler ? handler : rc_client_clock_get_now_millisecs;
    329 
    330 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    331   if (client->state.external_client && client->state.external_client->set_get_time_millisecs)
    332     client->state.external_client->set_get_time_millisecs(client, handler);
    333 #endif
    334 }
    335 
    336 int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle)
    337 {
    338   int aborted;
    339 
    340   rc_mutex_lock(&client->state.mutex);
    341   aborted = async_handle->aborted;
    342   rc_mutex_unlock(&client->state.mutex);
    343 
    344   return aborted;
    345 }
    346 
    347 static void rc_client_begin_async(rc_client_t* client, rc_client_async_handle_t* async_handle)
    348 {
    349   size_t i;
    350 
    351   rc_mutex_lock(&client->state.mutex);
    352   for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
    353     if (!client->state.async_handles[i]) {
    354       client->state.async_handles[i] = async_handle;
    355       break;
    356     }
    357   }
    358   rc_mutex_unlock(&client->state.mutex);
    359 }
    360 
    361 static int rc_client_end_async(rc_client_t* client, rc_client_async_handle_t* async_handle)
    362 {
    363   int aborted = async_handle->aborted;
    364 
    365   /* if client was destroyed, mutex doesn't exist and we don't need to remove the handle from the collection */
    366   if (aborted != RC_CLIENT_ASYNC_DESTROYED) {
    367     size_t i;
    368 
    369     rc_mutex_lock(&client->state.mutex);
    370     for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
    371       if (client->state.async_handles[i] == async_handle) {
    372         client->state.async_handles[i] = NULL;
    373         break;
    374       }
    375     }
    376     aborted = async_handle->aborted;
    377 
    378     rc_mutex_unlock(&client->state.mutex);
    379   }
    380 
    381   return aborted;
    382 }
    383 
    384 void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle)
    385 {
    386   if (async_handle && client) {
    387 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    388     if (client->state.external_client && client->state.external_client->abort_async) {
    389       client->state.external_client->abort_async(async_handle);
    390       return;
    391     }
    392 #endif
    393 
    394     rc_mutex_lock(&client->state.mutex);
    395     async_handle->aborted = RC_CLIENT_ASYNC_ABORTED;
    396     rc_mutex_unlock(&client->state.mutex);
    397   }
    398 }
    399 
    400 static int rc_client_async_handle_valid(rc_client_t* client, rc_client_async_handle_t* async_handle)
    401 {
    402   int valid = 0;
    403   size_t i;
    404 
    405   /* there is a small window of opportunity where the client could have been destroyed before calling
    406    * this function, but this function assumes the possibility that the handle has been destroyed, so
    407    * we can't check it for RC_CLIENT_ASYNC_DESTROYED before attempting to scan the client data */
    408   rc_mutex_lock(&client->state.mutex);
    409 
    410   for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
    411     if (client->state.async_handles[i] == async_handle) {
    412       valid = 1;
    413       break;
    414     }
    415   }
    416 
    417   rc_mutex_unlock(&client->state.mutex);
    418 
    419   return valid;
    420 }
    421 
    422 static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response)
    423 {
    424   if (!response->succeeded) {
    425     if (*result == RC_OK) {
    426       *result = RC_API_FAILURE;
    427       if (!response->error_message)
    428         return "Unexpected API failure with no error message";
    429     }
    430 
    431     if (response->error_message)
    432       return response->error_message;
    433   }
    434 
    435   (void)http_status_code;
    436 
    437   if (*result != RC_OK)
    438     return rc_error_str(*result);
    439 
    440   return NULL;
    441 }
    442 
    443 static void rc_client_raise_server_error_event(rc_client_t* client,
    444     const char* api, uint32_t related_id, int result, const char* error_message)
    445 {
    446   rc_client_server_error_t server_error;
    447   rc_client_event_t client_event;
    448 
    449   server_error.api = api;
    450   server_error.error_message = error_message;
    451   server_error.result = result;
    452   server_error.related_id = related_id;
    453 
    454   memset(&client_event, 0, sizeof(client_event));
    455   client_event.type = RC_CLIENT_EVENT_SERVER_ERROR;
    456   client_event.server_error = &server_error;
    457 
    458   client->callbacks.event_handler(&client_event, client);
    459 }
    460 
    461 static void rc_client_update_disconnect_state(rc_client_t* client)
    462 {
    463   rc_client_scheduled_callback_data_t* scheduled_callback;
    464   uint8_t new_state = RC_CLIENT_DISCONNECT_HIDDEN;
    465 
    466   rc_mutex_lock(&client->state.mutex);
    467 
    468   scheduled_callback = client->state.scheduled_callbacks;
    469   for (; scheduled_callback; scheduled_callback = scheduled_callback->next) {
    470     if (scheduled_callback->callback == rc_client_award_achievement_retry ||
    471       scheduled_callback->callback == rc_client_submit_leaderboard_entry_retry) {
    472       new_state = RC_CLIENT_DISCONNECT_VISIBLE;
    473       break;
    474     }
    475   }
    476 
    477   if ((client->state.disconnect & RC_CLIENT_DISCONNECT_VISIBLE) != new_state) {
    478     if (new_state == RC_CLIENT_DISCONNECT_VISIBLE)
    479       client->state.disconnect = RC_CLIENT_DISCONNECT_HIDDEN | RC_CLIENT_DISCONNECT_SHOW_PENDING;
    480     else
    481       client->state.disconnect = RC_CLIENT_DISCONNECT_VISIBLE | RC_CLIENT_DISCONNECT_HIDE_PENDING;
    482   }
    483   else {
    484     client->state.disconnect = new_state;
    485   }
    486 
    487   rc_mutex_unlock(&client->state.mutex);
    488 }
    489 
    490 static void rc_client_raise_disconnect_events(rc_client_t* client)
    491 {
    492   rc_client_event_t client_event;
    493   uint8_t new_state;
    494 
    495   rc_mutex_lock(&client->state.mutex);
    496 
    497   if (client->state.disconnect & RC_CLIENT_DISCONNECT_SHOW_PENDING)
    498     new_state = RC_CLIENT_DISCONNECT_VISIBLE;
    499   else
    500     new_state = RC_CLIENT_DISCONNECT_HIDDEN;
    501   client->state.disconnect = new_state;
    502 
    503   rc_mutex_unlock(&client->state.mutex);
    504 
    505   memset(&client_event, 0, sizeof(client_event));
    506   client_event.type = (new_state == RC_CLIENT_DISCONNECT_VISIBLE) ?
    507     RC_CLIENT_EVENT_DISCONNECTED : RC_CLIENT_EVENT_RECONNECTED;
    508   client->callbacks.event_handler(&client_event, client);
    509 }
    510 
    511 static int rc_client_should_retry(const rc_api_server_response_t* server_response)
    512 {
    513   switch (server_response->http_status_code) {
    514     case 502: /* 502 Bad Gateway */
    515       /* nginx connection pool full */
    516       return 1;
    517 
    518     case 503: /* 503 Service Temporarily Unavailable */
    519       /* site is in maintenance mode */
    520       return 1;
    521 
    522     case 504: /* 504 Gateway Timeout */
    523       /* timeout between web server and database server */
    524       return 1;
    525 
    526     case 429: /* 429 Too Many Requests */
    527       /* too many unlocks occurred at the same time */
    528       return 1;
    529 
    530     case 521: /* 521 Web Server is Down */
    531       /* cloudfare could not find the server */
    532       return 1;
    533 
    534     case 522: /* 522 Connection Timed Out */
    535       /* timeout connecting to server from cloudfare */
    536       return 1;
    537 
    538     case 523: /* 523 Origin is Unreachable */
    539       /* cloudfare cannot find server */
    540       return 1;
    541 
    542     case 524: /* 524 A Timeout Occurred */
    543       /* connection to server from cloudfare was dropped before request was completed */
    544       return 1;
    545 
    546     case 525: /* 525 SSL Handshake Failed */
    547       /* web server worker connection pool is exhausted */
    548       return 1;
    549 
    550     case RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR:
    551       /* client provided non-HTTP error (explicitly retryable) */
    552       return 1;
    553 
    554     case RC_API_SERVER_RESPONSE_CLIENT_ERROR:
    555       /* client provided non-HTTP error (implicitly non-retryable) */
    556       return 0;
    557 
    558     default:
    559       /* assume any error not handled above where no response was received should be retried */
    560       if (server_response->body_length == 0 || !server_response->body || !server_response->body[0])
    561         return 1;
    562 
    563       return 0;
    564   }
    565 }
    566 
    567 static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name)
    568 {
    569   rc_api_fetch_image_request_t image_request;
    570   rc_api_request_t request;
    571   int result;
    572 
    573   if (!buffer)
    574     return RC_INVALID_STATE;
    575 
    576   memset(&image_request, 0, sizeof(image_request));
    577   image_request.image_type = image_type;
    578   image_request.image_name = image_name;
    579   result = rc_api_init_fetch_image_request(&request, &image_request);
    580   if (result == RC_OK)
    581     snprintf(buffer, buffer_size, "%s", request.url);
    582 
    583   rc_api_destroy_request(&request);
    584   return result;
    585 }
    586 
    587 /* ===== User ===== */
    588 
    589 static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data)
    590 {
    591   rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data;
    592   rc_client_t* client = login_callback_data->client;
    593   rc_api_login_response_t login_response;
    594   rc_client_load_state_t* load_state;
    595   const char* error_message;
    596   int result;
    597 
    598   result = rc_client_end_async(client, &login_callback_data->async_handle);
    599   if (result) {
    600     if (result != RC_CLIENT_ASYNC_DESTROYED)
    601       rc_client_logout(client); /* logout will reset the user state and call the load game callback */
    602 
    603     free(login_callback_data);
    604     return;
    605   }
    606 
    607   if (client->state.user == RC_CLIENT_USER_STATE_NONE) {
    608     /* logout was called */
    609     if (login_callback_data->callback)
    610       login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata);
    611 
    612     free(login_callback_data);
    613     /* logout call will immediately abort load game before this callback gets called */
    614     return;
    615   }
    616 
    617   result = rc_api_process_login_server_response(&login_response, server_response);
    618   error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response);
    619   if (error_message) {
    620     rc_mutex_lock(&client->state.mutex);
    621     client->state.user = RC_CLIENT_USER_STATE_NONE;
    622     load_state = client->state.load;
    623     rc_mutex_unlock(&client->state.mutex);
    624 
    625     RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message);
    626     if (login_callback_data->callback)
    627       login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata);
    628 
    629     if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
    630       rc_client_begin_fetch_game_data(load_state);
    631   }
    632   else {
    633     client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username);
    634 
    635     if (strcmp(login_response.username, login_response.display_name) == 0)
    636       client->user.display_name = client->user.username;
    637     else
    638       client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name);
    639 
    640     client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token);
    641     client->user.score = login_response.score;
    642     client->user.score_softcore = login_response.score_softcore;
    643     client->user.num_unread_messages = login_response.num_unread_messages;
    644 
    645     rc_mutex_lock(&client->state.mutex);
    646     client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN;
    647     load_state = client->state.load;
    648     rc_mutex_unlock(&client->state.mutex);
    649 
    650     RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name);
    651 
    652     if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
    653       rc_client_begin_fetch_game_data(load_state);
    654 
    655     if (login_callback_data->callback)
    656       login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata);
    657   }
    658 
    659   rc_api_destroy_login_response(&login_response);
    660   free(login_callback_data);
    661 }
    662 
    663 static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client,
    664   const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata)
    665 {
    666   rc_client_generic_callback_data_t* callback_data;
    667   rc_api_request_t request;
    668   int result = rc_api_init_login_request(&request, login_request);
    669   const char* error_message = rc_error_str(result);
    670 
    671   if (result == RC_OK) {
    672     rc_mutex_lock(&client->state.mutex);
    673 
    674     if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) {
    675       error_message = "Login already in progress";
    676       result = RC_INVALID_STATE;
    677     }
    678     client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED;
    679 
    680     rc_mutex_unlock(&client->state.mutex);
    681   }
    682 
    683   if (result != RC_OK) {
    684     callback(result, error_message, client, callback_userdata);
    685     return NULL;
    686   }
    687 
    688   callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data));
    689   if (!callback_data) {
    690     callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
    691     return NULL;
    692   }
    693 
    694   callback_data->client = client;
    695   callback_data->callback = callback;
    696   callback_data->callback_userdata = callback_userdata;
    697 
    698   rc_client_begin_async(client, &callback_data->async_handle);
    699   client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client);
    700 
    701   rc_api_destroy_request(&request);
    702 
    703   /* if the user state has changed, the async operation completed synchronously */
    704   rc_mutex_lock(&client->state.mutex);
    705   if (client->state.user != RC_CLIENT_USER_STATE_LOGIN_REQUESTED)
    706     callback_data = NULL;
    707   rc_mutex_unlock(&client->state.mutex);
    708 
    709   return callback_data ? &callback_data->async_handle : NULL;
    710 }
    711 
    712 rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client,
    713   const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata)
    714 {
    715   rc_api_login_request_t login_request;
    716 
    717   if (!client) {
    718     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
    719     return NULL;
    720   }
    721 
    722   if (!username || !username[0]) {
    723     callback(RC_INVALID_STATE, "username is required", client, callback_userdata);
    724     return NULL;
    725   }
    726 
    727   if (!password || !password[0]) {
    728     callback(RC_INVALID_STATE, "password is required", client, callback_userdata);
    729     return NULL;
    730   }
    731 
    732 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    733   if (client->state.external_client && client->state.external_client->begin_login_with_password)
    734     return client->state.external_client->begin_login_with_password(client, username, password, callback, callback_userdata);
    735 #endif
    736 
    737   memset(&login_request, 0, sizeof(login_request));
    738   login_request.username = username;
    739   login_request.password = password;
    740 
    741   RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username);
    742   return rc_client_begin_login(client, &login_request, callback, callback_userdata);
    743 }
    744 
    745 rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client,
    746   const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata)
    747 {
    748   rc_api_login_request_t login_request;
    749 
    750   if (!client) {
    751     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
    752     return NULL;
    753   }
    754 
    755   if (!username || !username[0]) {
    756     callback(RC_INVALID_STATE, "username is required", client, callback_userdata);
    757     return NULL;
    758   }
    759 
    760   if (!token || !token[0]) {
    761     callback(RC_INVALID_STATE, "token is required", client, callback_userdata);
    762     return NULL;
    763   }
    764 
    765 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    766   if (client->state.external_client && client->state.external_client->begin_login_with_token)
    767     return client->state.external_client->begin_login_with_token(client, username, token, callback, callback_userdata);
    768 #endif
    769 
    770   memset(&login_request, 0, sizeof(login_request));
    771   login_request.username = username;
    772   login_request.api_token = token;
    773 
    774   RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username);
    775   return rc_client_begin_login(client, &login_request, callback, callback_userdata);
    776 }
    777 
    778 void rc_client_logout(rc_client_t* client)
    779 {
    780   rc_client_load_state_t* load_state;
    781 
    782   if (!client)
    783     return;
    784 
    785 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    786   if (client->state.external_client && client->state.external_client->logout) {
    787     client->state.external_client->logout();
    788     return;
    789   }
    790 #endif
    791 
    792   switch (client->state.user) {
    793     case RC_CLIENT_USER_STATE_LOGGED_IN:
    794       RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name);
    795       break;
    796 
    797     case RC_CLIENT_USER_STATE_LOGIN_REQUESTED:
    798       RC_CLIENT_LOG_INFO(client, "Aborting login");
    799       break;
    800   }
    801 
    802   rc_mutex_lock(&client->state.mutex);
    803 
    804   client->state.user = RC_CLIENT_USER_STATE_NONE;
    805   memset(&client->user, 0, sizeof(client->user));
    806 
    807   load_state = client->state.load;
    808 
    809   rc_mutex_unlock(&client->state.mutex);
    810 
    811   rc_client_unload_game(client);
    812 
    813   if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
    814     rc_client_load_error(load_state, RC_ABORTED, "Login aborted");
    815 }
    816 
    817 const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client)
    818 {
    819   if (!client)
    820     return NULL;
    821 
    822 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    823   if (client->state.external_client && client->state.external_client->get_user_info)
    824     return client->state.external_client->get_user_info();
    825 #endif
    826 
    827   return (client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL;
    828 }
    829 
    830 int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size)
    831 {
    832   if (!user)
    833     return RC_INVALID_STATE;
    834 
    835   return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name);
    836 }
    837 
    838 static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset,
    839     rc_client_user_game_summary_t* summary, const uint8_t unlock_bit)
    840 {
    841   rc_client_achievement_info_t* achievement = subset->achievements;
    842   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
    843   for (; achievement < stop; ++achievement) {
    844     switch (achievement->public_.category) {
    845       case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE:
    846         ++summary->num_core_achievements;
    847         summary->points_core += achievement->public_.points;
    848 
    849         if (achievement->public_.unlocked & unlock_bit) {
    850           ++summary->num_unlocked_achievements;
    851           summary->points_unlocked += achievement->public_.points;
    852         }
    853         if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) {
    854           ++summary->num_unsupported_achievements;
    855         }
    856 
    857         break;
    858 
    859       case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL:
    860         ++summary->num_unofficial_achievements;
    861         break;
    862 
    863       default:
    864         continue;
    865     }
    866   }
    867 }
    868 
    869 void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary)
    870 {
    871   const uint8_t unlock_bit = (client->state.hardcore) ?
    872     RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
    873 
    874   if (!summary)
    875     return;
    876 
    877   memset(summary, 0, sizeof(*summary));
    878   if (!client)
    879     return;
    880 
    881 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
    882   if (client->state.external_client && client->state.external_client->get_user_game_summary) {
    883     client->state.external_client->get_user_game_summary(summary);
    884     return;
    885   }
    886 #endif
    887 
    888   if (!rc_client_is_game_loaded(client))
    889     return;
    890 
    891   rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */
    892 
    893   rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit);
    894 
    895   rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */
    896 }
    897 
    898 /* ===== Game ===== */
    899 
    900 static void rc_client_free_game(rc_client_game_info_t* game)
    901 {
    902   rc_runtime_destroy(&game->runtime);
    903 
    904   rc_buffer_destroy(&game->buffer);
    905 
    906   free(game);
    907 }
    908 
    909 static void rc_client_free_load_state(rc_client_load_state_t* load_state)
    910 {
    911   if (load_state->game)
    912     rc_client_free_game(load_state->game);
    913 
    914   if (load_state->start_session_response) {
    915     rc_api_destroy_start_session_response(load_state->start_session_response);
    916     free(load_state->start_session_response);
    917   }
    918 
    919   free(load_state);
    920 }
    921 
    922 static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests)
    923 {
    924   rc_mutex_lock(&load_state->client->state.mutex);
    925 
    926   load_state->progress = state;
    927   load_state->outstanding_requests += num_requests;
    928 
    929   rc_mutex_unlock(&load_state->client->state.mutex);
    930 }
    931 
    932 static int rc_client_end_load_state(rc_client_load_state_t* load_state)
    933 {
    934   int remaining_requests = 0;
    935   int aborted = 0;
    936 
    937   rc_mutex_lock(&load_state->client->state.mutex);
    938 
    939   if (load_state->outstanding_requests > 0)
    940     --load_state->outstanding_requests;
    941   remaining_requests = load_state->outstanding_requests;
    942 
    943   if (load_state->client->state.load != load_state)
    944     aborted = 1;
    945 
    946   rc_mutex_unlock(&load_state->client->state.mutex);
    947 
    948   if (aborted) {
    949     /* we can't actually free the load_state itself if there are any outstanding requests
    950      * or their callbacks will try to use the free'd memory. As they call end_load_state,
    951      * the outstanding_requests count will reach zero and the memory will be free'd then. */
    952     if (remaining_requests == 0) {
    953       /* if one of the callbacks called rc_client_load_error, progress will be set to
    954        * RC_CLIENT_LOAD_STATE_ABORTED. There's no need to call the callback with RC_ABORTED
    955        * in that case, as it will have already been called with something more appropriate. */
    956       if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED && load_state->callback)
    957         load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata);
    958 
    959       rc_client_free_load_state(load_state);
    960     }
    961 
    962     return -1;
    963   }
    964 
    965   return remaining_requests;
    966 }
    967 
    968 static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message)
    969 {
    970   int remaining_requests = 0;
    971 
    972   rc_mutex_lock(&load_state->client->state.mutex);
    973 
    974   load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED;
    975   if (load_state->client->state.load == load_state)
    976     load_state->client->state.load = NULL;
    977 
    978   remaining_requests = load_state->outstanding_requests;
    979 
    980   rc_mutex_unlock(&load_state->client->state.mutex);
    981 
    982   RC_CLIENT_LOG_ERR_FORMATTED(load_state->client, "Load failed (%d): %s", result, error_message);
    983 
    984   if (load_state->callback)
    985     load_state->callback(result, error_message, load_state->client, load_state->callback_userdata);
    986 
    987   /* we can't actually free the load_state itself if there are any outstanding requests
    988    * or their callbacks will try to use the free'd memory. as they call end_load_state,
    989    * the outstanding_requests count will reach zero and the memory will be free'd then. */
    990   if (remaining_requests == 0)
    991     rc_client_free_load_state(load_state);
    992 }
    993 
    994 static void rc_client_load_aborted(rc_client_load_state_t* load_state)
    995 {
    996   /* prevent callback from being called when manually aborted */
    997   load_state->callback = NULL;
    998 
    999   /* mark the game as no longer being loaded */
   1000   rc_client_load_error(load_state, RC_ABORTED, NULL);
   1001 
   1002   /* decrement the async counter and potentially free the load_state object */
   1003   rc_client_end_load_state(load_state);
   1004 }
   1005 
   1006 static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref)
   1007 {
   1008   rc_client_subset_info_t* subset = game->subsets;
   1009   for (; subset; subset = subset->next) {
   1010     rc_client_achievement_info_t* achievement = subset->achievements;
   1011     rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   1012     for (; achievement < stop; ++achievement) {
   1013       if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED)
   1014         continue;
   1015 
   1016       if (rc_trigger_contains_memref(achievement->trigger, memref)) {
   1017         achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED;
   1018         achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED;
   1019 
   1020         if (achievement->trigger)
   1021           achievement->trigger->state = RC_TRIGGER_STATE_DISABLED;
   1022 
   1023         RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address);
   1024       }
   1025     }
   1026   }
   1027 }
   1028 
   1029 static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref)
   1030 {
   1031   rc_client_subset_info_t* subset = game->subsets;
   1032   for (; subset; subset = subset->next) {
   1033     rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
   1034     rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
   1035     for (; leaderboard < stop; ++leaderboard) {
   1036       if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED)
   1037         continue;
   1038       if (!leaderboard->lboard)
   1039         continue;
   1040 
   1041       if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref))
   1042         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
   1043       else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref))
   1044         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
   1045       else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref))
   1046         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
   1047       else if (rc_value_contains_memref(&leaderboard->lboard->value, memref))
   1048         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
   1049       else
   1050         continue;
   1051 
   1052       leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED;
   1053 
   1054       RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address);
   1055     }
   1056   }
   1057 }
   1058 
   1059 static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client)
   1060 {
   1061   const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id);
   1062   const uint32_t max_address = (regions && regions->num_regions > 0) ?
   1063       regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF;
   1064   uint8_t buffer[8];
   1065   uint32_t total_count = 0;
   1066   uint32_t invalid_count = 0;
   1067 
   1068   rc_memref_t** last_memref = &game->runtime.memrefs;
   1069   rc_memref_t* memref = game->runtime.memrefs;
   1070   for (; memref; memref = memref->next) {
   1071     if (!memref->value.is_indirect) {
   1072       total_count++;
   1073 
   1074       if (memref->address > max_address ||
   1075         client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) {
   1076         /* invalid address, remove from chain so we don't have to evaluate it in the future.
   1077          * it's still there, so anything referencing it will always fetch 0. */
   1078         *last_memref = memref->next;
   1079 
   1080         rc_client_invalidate_memref_achievements(game, client, memref);
   1081         rc_client_invalidate_memref_leaderboards(game, client, memref);
   1082 
   1083         invalid_count++;
   1084         continue;
   1085       }
   1086     }
   1087 
   1088     last_memref = &memref->next;
   1089   }
   1090 
   1091   game->max_valid_address = max_address;
   1092   RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count);
   1093 }
   1094 
   1095 static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count)
   1096 {
   1097   if (active_count > 0) {
   1098     rc_client_achievement_info_t* achievement;
   1099     rc_client_achievement_info_t* stop;
   1100     rc_runtime_trigger_t* trigger;
   1101     rc_client_subset_info_t* subset;
   1102 
   1103     if (active_count <= game->runtime.trigger_capacity) {
   1104       if (active_count != 0)
   1105         memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t));
   1106     } else {
   1107       if (game->runtime.triggers)
   1108         free(game->runtime.triggers);
   1109 
   1110       game->runtime.trigger_capacity = active_count;
   1111       game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t));
   1112     }
   1113 
   1114     trigger = game->runtime.triggers;
   1115     if (!trigger) {
   1116       /* malloc failed, no way to report error, just bail */
   1117       game->runtime.trigger_count = 0;
   1118       return;
   1119     }
   1120 
   1121     for (subset = game->subsets; subset; subset = subset->next) {
   1122       if (!subset->active)
   1123         continue;
   1124 
   1125       achievement = subset->achievements;
   1126       stop = achievement + subset->public_.num_achievements;
   1127 
   1128       for (; achievement < stop; ++achievement) {
   1129         if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) {
   1130           trigger->id = achievement->public_.id;
   1131           memcpy(trigger->md5, achievement->md5, 16);
   1132           trigger->trigger = achievement->trigger;
   1133           ++trigger;
   1134         }
   1135       }
   1136     }
   1137   }
   1138 
   1139   game->runtime.trigger_count = active_count;
   1140 }
   1141 
   1142 static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset)
   1143 {
   1144   rc_client_achievement_info_t* achievement = subset->achievements;
   1145   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   1146   uint32_t active_count = 0;
   1147 
   1148   for (; achievement < stop; ++achievement) {
   1149     if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
   1150       ++active_count;
   1151   }
   1152 
   1153   return active_count;
   1154 }
   1155 
   1156 void rc_client_update_active_achievements(rc_client_game_info_t* game)
   1157 {
   1158   uint32_t active_count = 0;
   1159   rc_client_subset_info_t* subset = game->subsets;
   1160   for (; subset; subset = subset->next) {
   1161     if (subset->active)
   1162       active_count += rc_client_subset_count_active_achievements(subset);
   1163   }
   1164 
   1165   rc_client_update_legacy_runtime_achievements(game, active_count);
   1166 }
   1167 
   1168 static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit)
   1169 {
   1170   rc_client_achievement_info_t* achievement = subset->achievements;
   1171   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   1172   uint32_t active_count = 0;
   1173 
   1174   for (; achievement < stop; ++achievement) {
   1175     if ((achievement->public_.unlocked & active_bit) == 0) {
   1176       switch (achievement->public_.state) {
   1177         case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED:
   1178         case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE:
   1179           rc_reset_trigger(achievement->trigger);
   1180           achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE;
   1181           ++active_count;
   1182           break;
   1183 
   1184         case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE:
   1185           ++active_count;
   1186           break;
   1187       }
   1188     }
   1189     else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE ||
   1190              achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) {
   1191 
   1192       /* if it's active despite being unlocked, and we're in encore mode, leave it active */
   1193       if (client->state.encore_mode) {
   1194         ++active_count;
   1195         continue;
   1196       }
   1197 
   1198       achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED;
   1199       achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ?
   1200         achievement->unlock_time_hardcore : achievement->unlock_time_softcore;
   1201 
   1202       if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) {
   1203         rc_client_event_t client_event;
   1204         memset(&client_event, 0, sizeof(client_event));
   1205         client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE;
   1206         client_event.achievement = &achievement->public_;
   1207         client->callbacks.event_handler(&client_event, client);
   1208       }
   1209 
   1210       if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state))
   1211         achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED;
   1212     }
   1213   }
   1214 
   1215   return active_count;
   1216 }
   1217 
   1218 static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit)
   1219 {
   1220   uint32_t active_count = 0;
   1221   rc_client_subset_info_t* subset = game->subsets;
   1222   for (; subset; subset = subset->next) {
   1223     if (subset->active)
   1224       active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit);
   1225   }
   1226 
   1227   rc_client_update_legacy_runtime_achievements(game, active_count);
   1228 }
   1229 
   1230 static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client)
   1231 {
   1232   const uint8_t active_bit = (client->state.encore_mode) ?
   1233       RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ?
   1234       RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
   1235 
   1236   rc_client_toggle_hardcore_achievements(game, client, active_bit);
   1237 }
   1238 
   1239 static void rc_client_update_legacy_runtime_leaderboards(rc_client_game_info_t* game, uint32_t active_count)
   1240 {
   1241   if (active_count > 0) {
   1242     rc_client_leaderboard_info_t* leaderboard;
   1243     rc_client_leaderboard_info_t* stop;
   1244     rc_client_subset_info_t* subset;
   1245     rc_runtime_lboard_t* lboard;
   1246 
   1247     if (active_count <= game->runtime.lboard_capacity) {
   1248       if (active_count != 0)
   1249         memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t));
   1250     } else {
   1251       if (game->runtime.lboards)
   1252         free(game->runtime.lboards);
   1253 
   1254       game->runtime.lboard_capacity = active_count;
   1255       game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t));
   1256     }
   1257 
   1258     lboard = game->runtime.lboards;
   1259     if (!lboard) {
   1260       /* malloc failed. no way to report error, just bail */
   1261       game->runtime.lboard_count = 0;
   1262       return;
   1263     }
   1264 
   1265     for (subset = game->subsets; subset; subset = subset->next) {
   1266       if (!subset->active)
   1267         continue;
   1268 
   1269       leaderboard = subset->leaderboards;
   1270       stop = leaderboard + subset->public_.num_leaderboards;
   1271       for (; leaderboard < stop; ++leaderboard) {
   1272         if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE ||
   1273             leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) {
   1274           lboard->id = leaderboard->public_.id;
   1275           memcpy(lboard->md5, leaderboard->md5, 16);
   1276           lboard->lboard = leaderboard->lboard;
   1277           ++lboard;
   1278         }
   1279       }
   1280     }
   1281   }
   1282 
   1283   game->runtime.lboard_count = active_count;
   1284 }
   1285 
   1286 void rc_client_update_active_leaderboards(rc_client_game_info_t* game)
   1287 {
   1288   rc_client_leaderboard_info_t* leaderboard;
   1289   rc_client_leaderboard_info_t* stop;
   1290 
   1291   uint32_t active_count = 0;
   1292   rc_client_subset_info_t* subset = game->subsets;
   1293   for (; subset; subset = subset->next)
   1294   {
   1295     if (!subset->active)
   1296       continue;
   1297 
   1298     leaderboard = subset->leaderboards;
   1299     stop = leaderboard + subset->public_.num_leaderboards;
   1300 
   1301     for (; leaderboard < stop; ++leaderboard)
   1302     {
   1303       switch (leaderboard->public_.state)
   1304       {
   1305         case RC_CLIENT_LEADERBOARD_STATE_ACTIVE:
   1306         case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
   1307           ++active_count;
   1308           break;
   1309       }
   1310     }
   1311   }
   1312 
   1313   rc_client_update_legacy_runtime_leaderboards(game, active_count);
   1314 }
   1315 
   1316 static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client)
   1317 {
   1318   rc_client_leaderboard_info_t* leaderboard;
   1319   rc_client_leaderboard_info_t* stop;
   1320   const uint8_t leaderboards_allowed =
   1321       client->state.hardcore || client->state.allow_leaderboards_in_softcore;
   1322 
   1323   uint32_t active_count = 0;
   1324   rc_client_subset_info_t* subset = game->subsets;
   1325   for (; subset; subset = subset->next) {
   1326     if (!subset->active)
   1327       continue;
   1328 
   1329     leaderboard = subset->leaderboards;
   1330     stop = leaderboard + subset->public_.num_leaderboards;
   1331 
   1332     for (; leaderboard < stop; ++leaderboard) {
   1333       switch (leaderboard->public_.state) {
   1334         case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
   1335           continue;
   1336 
   1337         case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
   1338           if (leaderboards_allowed) {
   1339             rc_reset_lboard(leaderboard->lboard);
   1340             leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
   1341             ++active_count;
   1342           }
   1343           break;
   1344 
   1345         default:
   1346           if (leaderboards_allowed)
   1347             ++active_count;
   1348           else
   1349             leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE;
   1350           break;
   1351       }
   1352     }
   1353   }
   1354 
   1355   rc_client_update_legacy_runtime_leaderboards(game, active_count);
   1356 }
   1357 
   1358 static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client)
   1359 {
   1360   rc_client_leaderboard_info_t* leaderboard;
   1361   rc_client_leaderboard_info_t* stop;
   1362 
   1363   rc_client_subset_info_t* subset = game->subsets;
   1364   for (; subset; subset = subset->next) {
   1365     if (!subset->active)
   1366       continue;
   1367 
   1368     leaderboard = subset->leaderboards;
   1369     stop = leaderboard + subset->public_.num_leaderboards;
   1370 
   1371     for (; leaderboard < stop; ++leaderboard) {
   1372       switch (leaderboard->public_.state) {
   1373         case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
   1374         case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
   1375           continue;
   1376 
   1377         case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
   1378           rc_client_release_leaderboard_tracker(client->game, leaderboard);
   1379           /* fallthrough */ /* to default */
   1380         default:
   1381           leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE;
   1382           break;
   1383       }
   1384     }
   1385   }
   1386 
   1387   game->runtime.lboard_count = 0;
   1388 }
   1389 
   1390 static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, rc_api_unlock_entry_t* unlocks, uint32_t num_unlocks, uint8_t mode)
   1391 {
   1392   rc_client_achievement_info_t* start = subset->achievements;
   1393   rc_client_achievement_info_t* stop = start + subset->public_.num_achievements;
   1394   rc_client_achievement_info_t* scan;
   1395   rc_api_unlock_entry_t* unlock = unlocks;
   1396   rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks;
   1397 
   1398   for (; unlock < unlock_stop; ++unlock) {
   1399     for (scan = start; scan < stop; ++scan) {
   1400       if (scan->public_.id == unlock->achievement_id) {
   1401         scan->public_.unlocked |= mode;
   1402 
   1403         if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE)
   1404           scan->unlock_time_hardcore = unlock->when;
   1405         if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE)
   1406           scan->unlock_time_softcore = unlock->when;
   1407 
   1408         if (scan == start)
   1409           ++start;
   1410         else if (scan + 1 == stop)
   1411           --stop;
   1412         break;
   1413       }
   1414     }
   1415   }
   1416 }
   1417 
   1418 static void rc_client_free_pending_media(rc_client_pending_media_t* pending_media)
   1419 {
   1420   if (pending_media->hash)
   1421     free((void*)pending_media->hash);
   1422 #ifdef RC_CLIENT_SUPPORTS_HASH
   1423   if (pending_media->data)
   1424     free(pending_media->data);
   1425   free((void*)pending_media->file_path);
   1426 #endif
   1427   free(pending_media);
   1428 }
   1429 
   1430 static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response)
   1431 {
   1432   rc_client_t* client = load_state->client;
   1433 
   1434   rc_mutex_lock(&client->state.mutex);
   1435   load_state->progress = (client->state.load == load_state) ?
   1436       RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED;
   1437   client->state.load = NULL;
   1438   rc_mutex_unlock(&client->state.mutex);
   1439 
   1440   if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) {
   1441     /* previous load state was aborted */
   1442     if (load_state->callback)
   1443       load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
   1444   }
   1445   else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
   1446     /* unlocks not available - assume malloc failed */
   1447     if (load_state->callback)
   1448       load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata);
   1449   }
   1450   else {
   1451     if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
   1452       rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks,
   1453           start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH);
   1454       rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks,
   1455           start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
   1456     }
   1457 
   1458     rc_mutex_lock(&client->state.mutex);
   1459     if (client->state.load == NULL)
   1460       client->game = load_state->game;
   1461     rc_mutex_unlock(&client->state.mutex);
   1462 
   1463     if (client->game != load_state->game) {
   1464       /* previous load state was aborted */
   1465       if (load_state->callback)
   1466         load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
   1467     }
   1468     else {
   1469       /* if a change media request is pending, kick it off */
   1470       rc_client_pending_media_t* pending_media;
   1471 
   1472       rc_mutex_lock(&load_state->client->state.mutex);
   1473       pending_media = load_state->pending_media;
   1474       load_state->pending_media = NULL;
   1475       rc_mutex_unlock(&load_state->client->state.mutex);
   1476 
   1477       if (pending_media) {
   1478         if (pending_media->hash) {
   1479           rc_client_begin_change_media_from_hash(client, pending_media->hash,
   1480             pending_media->callback, pending_media->callback_userdata);
   1481         } else {
   1482 #ifdef RC_CLIENT_SUPPORTS_HASH
   1483           rc_client_begin_change_media(client, pending_media->file_path,
   1484             pending_media->data, pending_media->data_size,
   1485             pending_media->callback, pending_media->callback_userdata);
   1486 #endif
   1487         }
   1488         rc_client_free_pending_media(pending_media);
   1489       }
   1490 
   1491       /* client->game must be set before calling this function so it can query the console_id */
   1492       rc_client_validate_addresses(load_state->game, client);
   1493 
   1494       rc_client_activate_achievements(load_state->game, client);
   1495       rc_client_activate_leaderboards(load_state->game, client);
   1496 
   1497       if (load_state->hash->hash[0] != '[') {
   1498         if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) {
   1499           /* schedule the periodic ping */
   1500           rc_client_scheduled_callback_data_t* callback_data = rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t));
   1501           memset(callback_data, 0, sizeof(*callback_data));
   1502           callback_data->callback = rc_client_ping;
   1503           callback_data->related_id = load_state->game->public_.id;
   1504           callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000;
   1505           rc_client_schedule_callback(client, callback_data);
   1506         }
   1507 
   1508         RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcore %s%s", load_state->game->public_.id,
   1509             client->state.hardcore ? "enabled" : "disabled",
   1510             (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : "");
   1511       }
   1512       else {
   1513         RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id);
   1514       }
   1515 
   1516       if (load_state->callback)
   1517         load_state->callback(RC_OK, NULL, client, load_state->callback_userdata);
   1518 
   1519       /* detach the game object so it doesn't get freed by free_load_state */
   1520       load_state->game = NULL;
   1521     }
   1522   }
   1523 
   1524   rc_client_free_load_state(load_state);
   1525 }
   1526 
   1527 static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data)
   1528 {
   1529   rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
   1530   rc_api_start_session_response_t start_session_response;
   1531   int outstanding_requests;
   1532   const char* error_message;
   1533   int result;
   1534 
   1535   result = rc_client_end_async(load_state->client, &load_state->async_handle);
   1536   if (result) {
   1537     if (result != RC_CLIENT_ASYNC_DESTROYED) {
   1538       rc_client_t* client = load_state->client;
   1539       rc_client_load_aborted(load_state);
   1540       RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session");
   1541     } else {
   1542       rc_client_free_load_state(load_state);
   1543     }
   1544     return;
   1545   }
   1546 
   1547   result = rc_api_process_start_session_server_response(&start_session_response, server_response);
   1548   error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response);
   1549   outstanding_requests = rc_client_end_load_state(load_state);
   1550 
   1551   if (error_message) {
   1552     rc_client_load_error(callback_data, result, error_message);
   1553   }
   1554   else if (outstanding_requests < 0) {
   1555     /* previous load state was aborted, load_state was free'd */
   1556   }
   1557   else if (outstanding_requests == 0) {
   1558     rc_client_activate_game(load_state, &start_session_response);
   1559   }
   1560   else {
   1561     load_state->start_session_response =
   1562         (rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t));
   1563 
   1564     if (!load_state->start_session_response) {
   1565       rc_client_load_error(callback_data, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
   1566     }
   1567     else {
   1568       /* safer to parse the response again than to try to copy it */
   1569       rc_api_process_start_session_response(load_state->start_session_response, server_response->body);
   1570     }
   1571   }
   1572 
   1573   rc_api_destroy_start_session_response(&start_session_response);
   1574 }
   1575 
   1576 static void rc_client_begin_start_session(rc_client_load_state_t* load_state)
   1577 {
   1578   rc_api_start_session_request_t start_session_params;
   1579   rc_client_t* client = load_state->client;
   1580   rc_api_request_t start_session_request;
   1581   int result;
   1582 
   1583   memset(&start_session_params, 0, sizeof(start_session_params));
   1584   start_session_params.username = client->user.username;
   1585   start_session_params.api_token = client->user.token;
   1586   start_session_params.game_id = load_state->hash->game_id;
   1587   start_session_params.game_hash = load_state->hash->hash;
   1588   start_session_params.hardcore = client->state.hardcore;
   1589 
   1590   result = rc_api_init_start_session_request(&start_session_request, &start_session_params);
   1591   if (result != RC_OK) {
   1592     rc_client_load_error(load_state, result, rc_error_str(result));
   1593   }
   1594   else {
   1595     rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1);
   1596     RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id);
   1597     rc_client_begin_async(client, &load_state->async_handle);
   1598     client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client);
   1599     rc_api_destroy_request(&start_session_request);
   1600   }
   1601 }
   1602 
   1603 static void rc_client_copy_achievements(rc_client_load_state_t* load_state,
   1604     rc_client_subset_info_t* subset,
   1605     const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements)
   1606 {
   1607   const rc_api_achievement_definition_t* read;
   1608   const rc_api_achievement_definition_t* stop;
   1609   rc_client_achievement_info_t* achievements;
   1610   rc_client_achievement_info_t* achievement;
   1611   rc_client_achievement_info_t* scan;
   1612   rc_buffer_t* buffer;
   1613   rc_parse_state_t parse;
   1614   const char* memaddr;
   1615   size_t size;
   1616   int trigger_size;
   1617 
   1618   subset->achievements = NULL;
   1619   subset->public_.num_achievements = num_achievements;
   1620 
   1621   if (num_achievements == 0)
   1622     return;
   1623 
   1624   stop = achievement_definitions + num_achievements;
   1625 
   1626   /* if not testing unofficial, filter them out */
   1627   if (!load_state->client->state.unofficial_enabled) {
   1628     for (read = achievement_definitions; read < stop; ++read) {
   1629       if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE)
   1630         --num_achievements;
   1631     }
   1632 
   1633     subset->public_.num_achievements = num_achievements;
   1634 
   1635     if (num_achievements == 0)
   1636       return;
   1637   }
   1638 
   1639   /* preallocate space for achievements */
   1640   size = 24 /* assume average title length of 24 */
   1641       + 48 /* assume average description length of 48 */
   1642       + sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */
   1643       + sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */
   1644       + sizeof(rc_client_achievement_info_t);
   1645   rc_buffer_reserve(&load_state->game->buffer, size * num_achievements);
   1646 
   1647   /* allocate the achievement array */
   1648   size = sizeof(rc_client_achievement_info_t) * num_achievements;
   1649   buffer = &load_state->game->buffer;
   1650   achievement = achievements = rc_buffer_alloc(buffer, size);
   1651   memset(achievements, 0, size);
   1652 
   1653   /* copy the achievement data */
   1654   for (read = achievement_definitions; read < stop; ++read) {
   1655     if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled)
   1656       continue;
   1657 
   1658     achievement->public_.title = rc_buffer_strcpy(buffer, read->title);
   1659     achievement->public_.description = rc_buffer_strcpy(buffer, read->description);
   1660     snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name);
   1661     achievement->public_.id = read->id;
   1662     achievement->public_.points = read->points;
   1663     achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ?
   1664       RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE;
   1665     achievement->public_.rarity = read->rarity;
   1666     achievement->public_.rarity_hardcore = read->rarity_hardcore;
   1667     achievement->public_.type = read->type; /* assert: mapping is 1:1 */
   1668 
   1669     memaddr = read->definition;
   1670     rc_runtime_checksum(memaddr, achievement->md5);
   1671 
   1672     trigger_size = rc_trigger_size(memaddr);
   1673     if (trigger_size < 0) {
   1674       RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id);
   1675       achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED;
   1676       achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED;
   1677     }
   1678     else {
   1679       /* populate the item, using the communal memrefs pool */
   1680       rc_init_parse_state(&parse, rc_buffer_reserve(buffer, trigger_size), NULL, 0);
   1681       parse.first_memref = &load_state->game->runtime.memrefs;
   1682       parse.variables = &load_state->game->runtime.variables;
   1683       achievement->trigger = RC_ALLOC(rc_trigger_t, &parse);
   1684       rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse);
   1685 
   1686       if (parse.offset < 0) {
   1687         RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id);
   1688         achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED;
   1689         achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED;
   1690       }
   1691       else {
   1692         rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset);
   1693         achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */
   1694       }
   1695 
   1696       rc_destroy_parse_state(&parse);
   1697     }
   1698 
   1699     achievement->created_time = read->created;
   1700     achievement->updated_time = read->updated;
   1701 
   1702     scan = achievement;
   1703     while (scan > achievements) {
   1704       --scan;
   1705       if (strcmp(scan->author, read->author) == 0) {
   1706         achievement->author = scan->author;
   1707         break;
   1708       }
   1709     }
   1710     if (!achievement->author)
   1711       achievement->author = rc_buffer_strcpy(buffer, read->author);
   1712 
   1713     ++achievement;
   1714   }
   1715 
   1716   subset->achievements = achievements;
   1717 }
   1718 
   1719 uint8_t rc_client_map_leaderboard_format(int format)
   1720 {
   1721   switch (format) {
   1722     case RC_FORMAT_SECONDS:
   1723     case RC_FORMAT_CENTISECS:
   1724     case RC_FORMAT_MINUTES:
   1725     case RC_FORMAT_SECONDS_AS_MINUTES:
   1726     case RC_FORMAT_FRAMES:
   1727       return RC_CLIENT_LEADERBOARD_FORMAT_TIME;
   1728 
   1729     case RC_FORMAT_SCORE:
   1730       return RC_CLIENT_LEADERBOARD_FORMAT_SCORE;
   1731 
   1732     case RC_FORMAT_VALUE:
   1733     case RC_FORMAT_FLOAT1:
   1734     case RC_FORMAT_FLOAT2:
   1735     case RC_FORMAT_FLOAT3:
   1736     case RC_FORMAT_FLOAT4:
   1737     case RC_FORMAT_FLOAT5:
   1738     case RC_FORMAT_FLOAT6:
   1739     case RC_FORMAT_FIXED1:
   1740     case RC_FORMAT_FIXED2:
   1741     case RC_FORMAT_FIXED3:
   1742     case RC_FORMAT_TENS:
   1743     case RC_FORMAT_HUNDREDS:
   1744     case RC_FORMAT_THOUSANDS:
   1745     case RC_FORMAT_UNSIGNED_VALUE:
   1746     default:
   1747       return RC_CLIENT_LEADERBOARD_FORMAT_VALUE;
   1748   }
   1749 }
   1750 
   1751 static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state,
   1752     rc_client_subset_info_t* subset,
   1753     const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards)
   1754 {
   1755   const rc_api_leaderboard_definition_t* read;
   1756   const rc_api_leaderboard_definition_t* stop;
   1757   rc_client_leaderboard_info_t* leaderboards;
   1758   rc_client_leaderboard_info_t* leaderboard;
   1759   rc_buffer_t* buffer;
   1760   rc_parse_state_t parse;
   1761   const char* memaddr;
   1762   const char* ptr;
   1763   size_t size;
   1764   int lboard_size;
   1765 
   1766   subset->leaderboards = NULL;
   1767   subset->public_.num_leaderboards = num_leaderboards;
   1768 
   1769   if (num_leaderboards == 0)
   1770     return;
   1771 
   1772   /* preallocate space for achievements */
   1773   size = 24 /* assume average title length of 24 */
   1774       + 48 /* assume average description length of 48 */
   1775       + sizeof(rc_lboard_t) /* lboard container */
   1776       + (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */
   1777       + (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */
   1778       + sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */
   1779       + sizeof(rc_client_leaderboard_info_t);
   1780   rc_buffer_reserve(&load_state->game->buffer, size * num_leaderboards);
   1781 
   1782   /* allocate the achievement array */
   1783   size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards;
   1784   buffer = &load_state->game->buffer;
   1785   leaderboard = leaderboards = rc_buffer_alloc(buffer, size);
   1786   memset(leaderboards, 0, size);
   1787 
   1788   /* copy the achievement data */
   1789   read = leaderboard_definitions;
   1790   stop = read + num_leaderboards;
   1791   do {
   1792     leaderboard->public_.title = rc_buffer_strcpy(buffer, read->title);
   1793     leaderboard->public_.description = rc_buffer_strcpy(buffer, read->description);
   1794     leaderboard->public_.id = read->id;
   1795     leaderboard->public_.format = rc_client_map_leaderboard_format(read->format);
   1796     leaderboard->public_.lower_is_better = read->lower_is_better;
   1797     leaderboard->format = (uint8_t)read->format;
   1798     leaderboard->hidden = (uint8_t)read->hidden;
   1799 
   1800     memaddr = read->definition;
   1801     rc_runtime_checksum(memaddr, leaderboard->md5);
   1802 
   1803     ptr = strstr(memaddr, "VAL:");
   1804     if (ptr != NULL) {
   1805       /* calculate the DJB2 hash of the VAL portion of the string*/
   1806       uint32_t hash = 5381;
   1807       ptr += 4; /* skip 'VAL:' */
   1808       while (*ptr && (ptr[0] != ':' || ptr[1] != ':'))
   1809          hash = (hash << 5) + hash + *ptr++;
   1810       leaderboard->value_djb2 = hash;
   1811     }
   1812 
   1813     lboard_size = rc_lboard_size(memaddr);
   1814     if (lboard_size < 0) {
   1815       RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id);
   1816       leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
   1817     }
   1818     else {
   1819       /* populate the item, using the communal memrefs pool */
   1820       rc_init_parse_state(&parse, rc_buffer_reserve(buffer, lboard_size), NULL, 0);
   1821       parse.first_memref = &load_state->game->runtime.memrefs;
   1822       parse.variables = &load_state->game->runtime.variables;
   1823       leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse);
   1824       rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse);
   1825 
   1826       if (parse.offset < 0) {
   1827         RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id);
   1828         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
   1829       }
   1830       else {
   1831         rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset);
   1832         leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */
   1833       }
   1834 
   1835       rc_destroy_parse_state(&parse);
   1836     }
   1837 
   1838     ++leaderboard;
   1839     ++read;
   1840   } while (read < stop);
   1841 
   1842   subset->leaderboards = leaderboards;
   1843 }
   1844 
   1845 static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title)
   1846 {
   1847   const char* subset_prefix = strstr(title, "[Subset - ");
   1848   if (subset_prefix) {
   1849     const char* start = subset_prefix + 10;
   1850     const char* stop = strstr(start, "]");
   1851     const size_t len = stop - start;
   1852     char* result = (char*)rc_buffer_alloc(&game->buffer, len + 1);
   1853 
   1854     memcpy(result, start, len);
   1855     result[len] = '\0';
   1856     return result;
   1857   }
   1858 
   1859   return NULL;
   1860 }
   1861 
   1862 static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data)
   1863 {
   1864   rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
   1865   rc_api_fetch_game_data_response_t fetch_game_data_response;
   1866   int outstanding_requests;
   1867   const char* error_message;
   1868   int result;
   1869 
   1870   result = rc_client_end_async(load_state->client, &load_state->async_handle);
   1871   if (result) {
   1872     if (result != RC_CLIENT_ASYNC_DESTROYED) {
   1873       rc_client_t* client = load_state->client;
   1874       rc_client_load_aborted(load_state);
   1875       RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data");
   1876     } else {
   1877       rc_client_free_load_state(load_state);
   1878     }
   1879     return;
   1880   }
   1881 
   1882   result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response);
   1883   error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response);
   1884 
   1885   outstanding_requests = rc_client_end_load_state(load_state);
   1886 
   1887   if (error_message) {
   1888     rc_client_load_error(load_state, result, error_message);
   1889   }
   1890   else if (outstanding_requests < 0) {
   1891     /* previous load state was aborted, load_state was free'd */
   1892   }
   1893   else {
   1894     rc_client_subset_info_t* subset;
   1895 
   1896     subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t));
   1897     memset(subset, 0, sizeof(*subset));
   1898     subset->public_.id = fetch_game_data_response.id;
   1899     subset->active = 1;
   1900     snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", fetch_game_data_response.image_name);
   1901     load_state->subset = subset;
   1902 
   1903     if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN &&
   1904         fetch_game_data_response.console_id != load_state->game->public_.console_id) {
   1905       RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u",
   1906         fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id);
   1907     }
   1908 
   1909     /* kick off the start session request while we process the game data */
   1910     rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1);
   1911     if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
   1912       /* we can't unlock achievements without a session, lock spectator mode for the game */
   1913       load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED;
   1914     }
   1915     else {
   1916       rc_client_begin_start_session(load_state);
   1917     }
   1918 
   1919     /* process the game data */
   1920     rc_client_copy_achievements(load_state, subset,
   1921         fetch_game_data_response.achievements, fetch_game_data_response.num_achievements);
   1922     rc_client_copy_leaderboards(load_state, subset,
   1923         fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards);
   1924 
   1925     if (!load_state->game->subsets) {
   1926       /* core set */
   1927       rc_mutex_lock(&load_state->client->state.mutex);
   1928       load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title);
   1929       load_state->game->subsets = subset;
   1930       load_state->game->public_.badge_name = subset->public_.badge_name;
   1931       load_state->game->public_.console_id = fetch_game_data_response.console_id;
   1932       rc_mutex_unlock(&load_state->client->state.mutex);
   1933 
   1934       subset->public_.title = load_state->game->public_.title;
   1935 
   1936       if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) {
   1937         result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0);
   1938         if (result != RC_OK) {
   1939           RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result);
   1940         }
   1941       }
   1942     }
   1943     else {
   1944       rc_client_subset_info_t* scan;
   1945 
   1946       /* subset - extract subset title */
   1947       subset->public_.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title);
   1948       if (!subset->public_.title) {
   1949         const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public_.title);
   1950         if (core_subset_title) {
   1951           scan = load_state->game->subsets;
   1952           for (; scan; scan = scan->next) {
   1953             if (scan->public_.title == load_state->game->public_.title) {
   1954               scan->public_.title = core_subset_title;
   1955               break;
   1956             }
   1957           }
   1958         }
   1959 
   1960         subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title);
   1961       }
   1962 
   1963       /* append to subset list */
   1964       scan = load_state->game->subsets;
   1965       while (scan->next)
   1966         scan = scan->next;
   1967       scan->next = subset;
   1968     }
   1969 
   1970     if (load_state->client->callbacks.post_process_game_data_response) {
   1971       load_state->client->callbacks.post_process_game_data_response(server_response,
   1972         &fetch_game_data_response, load_state->client, load_state->callback_userdata);
   1973     }
   1974 
   1975     outstanding_requests = rc_client_end_load_state(load_state);
   1976     if (outstanding_requests < 0) {
   1977       /* previous load state was aborted, load_state was free'd */
   1978     }
   1979     else {
   1980       if (outstanding_requests == 0)
   1981         rc_client_activate_game(load_state, load_state->start_session_response);
   1982     }
   1983   }
   1984 
   1985   rc_api_destroy_fetch_game_data_response(&fetch_game_data_response);
   1986 }
   1987 
   1988 static rc_client_game_info_t* rc_client_allocate_game(void)
   1989 {
   1990   rc_client_game_info_t* game = (rc_client_game_info_t*)calloc(1, sizeof(*game));
   1991   if (!game)
   1992     return NULL;
   1993 
   1994   rc_buffer_init(&game->buffer);
   1995   rc_runtime_init(&game->runtime);
   1996 
   1997   return game;
   1998 }
   1999 
   2000 static int rc_client_attach_load_state(rc_client_t* client, rc_client_load_state_t* load_state)
   2001 {
   2002   if (client->state.load == NULL) {
   2003     rc_client_unload_game(client);
   2004     client->state.load = load_state;
   2005 
   2006     if (load_state->game == NULL) {
   2007       load_state->game = rc_client_allocate_game();
   2008       if (!load_state->game) {
   2009         if (load_state->callback)
   2010           load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata);
   2011 
   2012         return 0;
   2013       }
   2014     }
   2015   }
   2016   else if (client->state.load != load_state) {
   2017     /* previous load was aborted */
   2018     if (load_state->callback)
   2019       load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
   2020 
   2021     return 0;
   2022   }
   2023 
   2024   return 1;
   2025 }
   2026 
   2027 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2028 
   2029 static void rc_client_external_load_state_callback(int result, const char* error_message, rc_client_t* client, void* userdata)
   2030 {
   2031   rc_client_load_state_t* load_state = (rc_client_load_state_t*)userdata;
   2032   int async_aborted;
   2033 
   2034   client = load_state->client;
   2035   async_aborted = rc_client_end_async(client, &load_state->async_handle);
   2036   if (async_aborted) {
   2037     if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) {
   2038       RC_CLIENT_LOG_VERBOSE(client, "Load aborted during external loading");
   2039     }
   2040 
   2041     rc_client_unload_game(client); /* unload the game from the external client */
   2042     rc_client_free_load_state(load_state);
   2043     return;
   2044   }
   2045 
   2046   if (result != RC_OK) {
   2047     rc_client_load_error(load_state, result, error_message);
   2048     return;
   2049   }
   2050 
   2051   rc_mutex_lock(&client->state.mutex);
   2052   load_state->progress = (client->state.load == load_state) ?
   2053     RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED;
   2054   client->state.load = NULL;
   2055   rc_mutex_unlock(&client->state.mutex);
   2056 
   2057   if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) {
   2058     /* previous load state was aborted */
   2059     if (load_state->callback)
   2060       load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
   2061   }
   2062   else {
   2063     /* keep partial game object for media_hash management */
   2064     if (client->state.external_client && client->state.external_client->get_game_info) {
   2065       const rc_client_game_t* info = client->state.external_client->get_game_info();
   2066       load_state->game->public_.console_id = info->console_id;
   2067       client->game = load_state->game;
   2068       load_state->game = NULL;
   2069     }
   2070 
   2071     if (load_state->callback)
   2072       load_state->callback(RC_OK, NULL, client, load_state->callback_userdata);
   2073   }
   2074 
   2075   rc_client_free_load_state(load_state);
   2076 }
   2077 
   2078 #endif
   2079 
   2080 static void rc_client_process_resolved_hash(rc_client_load_state_t* load_state)
   2081 {
   2082   rc_client_t* client = load_state->client;
   2083 
   2084   if (load_state->hash->game_id == 0) {
   2085 #ifdef RC_CLIENT_SUPPORTS_HASH
   2086     char hash[33];
   2087 
   2088     if (rc_hash_iterate(hash, &load_state->hash_iterator)) {
   2089       /* found another hash to try */
   2090       load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1];
   2091       rc_client_load_game(load_state, hash, NULL);
   2092       return;
   2093     }
   2094 
   2095     if (load_state->game->media_hash &&
   2096         load_state->game->media_hash->game_hash &&
   2097         load_state->game->media_hash->game_hash->next) {
   2098       /* multiple hashes were tried, create a CSV */
   2099       struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash;
   2100       int count = 1;
   2101       char* ptr;
   2102       size_t size;
   2103 
   2104       size = strlen(game_hash->hash) + 1;
   2105       while (game_hash->next) {
   2106         game_hash = game_hash->next;
   2107         size += strlen(game_hash->hash) + 1;
   2108         count++;
   2109       }
   2110 
   2111       ptr = (char*)rc_buffer_alloc(&load_state->game->buffer, size);
   2112       ptr += size - 1;
   2113       *ptr = '\0';
   2114       game_hash = load_state->game->media_hash->game_hash;
   2115       do {
   2116         const size_t hash_len = strlen(game_hash->hash);
   2117         ptr -= hash_len;
   2118         memcpy(ptr, game_hash->hash, hash_len);
   2119 
   2120         game_hash = game_hash->next;
   2121         if (!game_hash)
   2122           break;
   2123 
   2124         ptr--;
   2125         *ptr = ',';
   2126       } while (1);
   2127 
   2128       load_state->game->public_.hash = ptr;
   2129       load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN;
   2130     } else {
   2131       /* only a single hash was tried, capture it */
   2132       load_state->game->public_.console_id = load_state->hash_console_id;
   2133       load_state->game->public_.hash = load_state->hash->hash;
   2134 
   2135       if (client->callbacks.identify_unknown_hash) {
   2136         load_state->hash->game_id = client->callbacks.identify_unknown_hash(
   2137             load_state->hash_console_id, load_state->hash->hash, client, load_state->callback_userdata);
   2138 
   2139         if (load_state->hash->game_id != 0) {
   2140           RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Client says to load game %u for unidentified hash %s",
   2141             load_state->hash->game_id, load_state->hash->hash);
   2142         }
   2143       }
   2144     }
   2145 #else
   2146     load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN;
   2147     load_state->game->public_.hash = load_state->hash->hash;
   2148 #endif /* RC_CLIENT_SUPPORTS_HASH */
   2149 
   2150     if (load_state->hash->game_id == 0) {
   2151 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2152       if (client->state.external_client) {
   2153         if (client->state.external_client->load_unknown_game) {
   2154           client->state.external_client->load_unknown_game(load_state->game->public_.hash);
   2155           rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game");
   2156           return;
   2157         }
   2158         /* no external method specifically for unknown game, just pass the hash through to begin_load_game below */
   2159       }
   2160       else {
   2161 #endif
   2162         /* mimics rc_client_load_unknown_game without allocating a new game object */
   2163         rc_client_subset_info_t* subset;
   2164 
   2165         subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t));
   2166         memset(subset, 0, sizeof(*subset));
   2167         subset->public_.title = "";
   2168 
   2169         load_state->game->public_.title = "Unknown Game";
   2170         load_state->game->public_.badge_name = "";
   2171         load_state->game->subsets = subset;
   2172         client->game = load_state->game;
   2173         load_state->game = NULL;
   2174 
   2175         rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game");
   2176         return;
   2177 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2178       }
   2179 #endif
   2180     }
   2181   }
   2182 
   2183   if (load_state->hash->hash[0] != '[') { /* not [NO HASH] or [SUBSETxx] */
   2184     load_state->game->public_.id = load_state->hash->game_id;
   2185     load_state->game->public_.hash = load_state->hash->hash;
   2186   }
   2187 
   2188   /* done with the hashing code, release the global pointer */
   2189   g_hash_client = NULL;
   2190 
   2191 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2192   if (client->state.external_client) {
   2193     if (client->state.external_client->add_game_hash)
   2194       client->state.external_client->add_game_hash(load_state->hash->hash, load_state->hash->game_id);
   2195 
   2196     if (client->state.external_client->begin_load_game) {
   2197       rc_client_begin_async(client, &load_state->async_handle);
   2198       client->state.external_client->begin_load_game(client, load_state->hash->hash, rc_client_external_load_state_callback, load_state);
   2199     }
   2200     return;
   2201   }
   2202 #endif
   2203 
   2204   rc_client_begin_fetch_game_data(load_state);
   2205 }
   2206 
   2207 void rc_client_load_unknown_game(rc_client_t* client, const char* tried_hashes)
   2208 {
   2209   rc_client_subset_info_t* subset;
   2210   rc_client_game_info_t* game;
   2211 
   2212   game = rc_client_allocate_game();
   2213   if (!game)
   2214     return;
   2215 
   2216   subset = (rc_client_subset_info_t*)rc_buffer_alloc(&game->buffer, sizeof(rc_client_subset_info_t));
   2217   memset(subset, 0, sizeof(*subset));
   2218   subset->public_.title = "";
   2219   game->subsets = subset;
   2220 
   2221   game->public_.title = "Unknown Game";
   2222   game->public_.badge_name = "";
   2223   game->public_.console_id = RC_CONSOLE_UNKNOWN;
   2224 
   2225   if (strlen(tried_hashes) == 32) { /* only one hash tried, add it to the list */
   2226     rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, tried_hashes);
   2227     game_hash->game_id = 0;
   2228     game->public_.hash = game_hash->hash;
   2229   }
   2230   else {
   2231     game->public_.hash = rc_buffer_strcpy(&game->buffer, tried_hashes);
   2232   }
   2233 
   2234   rc_client_unload_game(client);
   2235   client->game = game;
   2236 }
   2237 
   2238 static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state)
   2239 {
   2240   rc_api_fetch_game_data_request_t fetch_game_data_request;
   2241   rc_client_t* client = load_state->client;
   2242   rc_api_request_t request;
   2243   int result;
   2244 
   2245   rc_mutex_lock(&client->state.mutex);
   2246   result = client->state.user;
   2247   if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED)
   2248     load_state->progress = RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN;
   2249   rc_mutex_unlock(&client->state.mutex);
   2250 
   2251   switch (result) {
   2252     case RC_CLIENT_USER_STATE_LOGGED_IN:
   2253       break;
   2254 
   2255     case RC_CLIENT_USER_STATE_LOGIN_REQUESTED:
   2256       /* do nothing, this function will be called again after login completes */
   2257       return;
   2258 
   2259     default:
   2260       rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED));
   2261       return;
   2262   }
   2263 
   2264   memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request));
   2265   fetch_game_data_request.username = client->user.username;
   2266   fetch_game_data_request.api_token = client->user.token;
   2267   fetch_game_data_request.game_id = load_state->hash->game_id;
   2268 
   2269   result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request);
   2270   if (result != RC_OK) {
   2271     rc_client_load_error(load_state, result, rc_error_str(result));
   2272     return;
   2273   }
   2274 
   2275   rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, 1);
   2276 
   2277   RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id);
   2278   rc_client_begin_async(client, &load_state->async_handle);
   2279   client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client);
   2280 
   2281   rc_api_destroy_request(&request);
   2282 }
   2283 
   2284 static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data)
   2285 {
   2286   rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
   2287   rc_client_t* client = load_state->client;
   2288   rc_api_resolve_hash_response_t resolve_hash_response;
   2289   int outstanding_requests;
   2290   const char* error_message;
   2291   int result;
   2292 
   2293   result = rc_client_end_async(client, &load_state->async_handle);
   2294   if (result) {
   2295     if (result != RC_CLIENT_ASYNC_DESTROYED) {
   2296       rc_client_load_aborted(load_state);
   2297       RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification");
   2298     } else {
   2299       rc_client_free_load_state(load_state);
   2300     }
   2301     return;
   2302   }
   2303 
   2304   result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response);
   2305   error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response);
   2306 
   2307   if (error_message) {
   2308     rc_client_end_load_state(load_state);
   2309     rc_client_load_error(load_state, result, error_message);
   2310   }
   2311   else {
   2312     /* hash exists outside the load state - always update it */
   2313     load_state->hash->game_id = resolve_hash_response.game_id;
   2314     RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash);
   2315 
   2316     /* have to call end_load_state after updating hash in case the load_state gets free'd */
   2317     outstanding_requests = rc_client_end_load_state(load_state);
   2318     if (outstanding_requests < 0) {
   2319       /* previous load state was aborted, load_state was free'd */
   2320     }
   2321     else {
   2322       rc_client_process_resolved_hash(load_state);
   2323     }
   2324   }
   2325 
   2326   rc_api_destroy_resolve_hash_response(&resolve_hash_response);
   2327 }
   2328 
   2329 rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash)
   2330 {
   2331   rc_client_game_hash_t* game_hash;
   2332 
   2333   rc_mutex_lock(&client->state.mutex);
   2334   game_hash = client->hashes;
   2335   while (game_hash) {
   2336     if (strcasecmp(game_hash->hash, hash) == 0)
   2337       break;
   2338 
   2339     game_hash = game_hash->next;
   2340   }
   2341 
   2342   if (!game_hash) {
   2343     game_hash = rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t));
   2344     memset(game_hash, 0, sizeof(*game_hash));
   2345     snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash);
   2346     game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID;
   2347     game_hash->next = client->hashes;
   2348     client->hashes = game_hash;
   2349   }
   2350   rc_mutex_unlock(&client->state.mutex);
   2351 
   2352   return game_hash;
   2353 }
   2354 
   2355 void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id)
   2356 {
   2357   /* store locally, even if passing to external client */
   2358   rc_client_game_hash_t* game_hash = rc_client_find_game_hash(client, hash);
   2359   game_hash->game_id = game_id;
   2360 
   2361 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2362   if (client->state.external_client && client->state.external_client->add_game_hash)
   2363     client->state.external_client->add_game_hash(hash, game_id);
   2364 #endif
   2365 }
   2366 
   2367 static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state,
   2368   const char* hash, const char* file_path)
   2369 {
   2370   rc_client_t* client = load_state->client;
   2371   rc_client_game_hash_t* old_hash;
   2372 
   2373   if (!rc_client_attach_load_state(client, load_state)) {
   2374     rc_client_free_load_state(load_state);
   2375     return NULL;
   2376   }
   2377 
   2378   old_hash = load_state->hash;
   2379   load_state->hash = rc_client_find_game_hash(client, hash);
   2380 
   2381   if (file_path) {
   2382     rc_client_media_hash_t* media_hash =
   2383         (rc_client_media_hash_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(*media_hash));
   2384     media_hash->game_hash = load_state->hash;
   2385     media_hash->path_djb2 = rc_djb2(file_path);
   2386     media_hash->next = load_state->game->media_hash;
   2387     load_state->game->media_hash = media_hash;
   2388   }
   2389   else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) {
   2390     load_state->game->media_hash->game_hash = load_state->hash;
   2391   }
   2392 
   2393   if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) {
   2394     rc_api_resolve_hash_request_t resolve_hash_request;
   2395     rc_api_request_t request;
   2396     int result;
   2397 
   2398     memset(&resolve_hash_request, 0, sizeof(resolve_hash_request));
   2399     resolve_hash_request.game_hash = hash;
   2400 
   2401     result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request);
   2402     if (result != RC_OK) {
   2403       rc_client_load_error(load_state, result, rc_error_str(result));
   2404       return NULL;
   2405     }
   2406 
   2407     rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1);
   2408 
   2409     rc_client_begin_async(client, &load_state->async_handle);
   2410     client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client);
   2411 
   2412     rc_api_destroy_request(&request);
   2413   }
   2414   else {
   2415     RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash);
   2416 
   2417     rc_client_process_resolved_hash(load_state);
   2418   }
   2419 
   2420   return (client->state.load == load_state) ? &load_state->async_handle : NULL;
   2421 }
   2422 
   2423 rc_client_async_handle_t* rc_client_begin_load_game(rc_client_t* client, const char* hash, rc_client_callback_t callback, void* callback_userdata)
   2424 {
   2425   rc_client_load_state_t* load_state;
   2426 
   2427   if (!client) {
   2428     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
   2429     return NULL;
   2430   }
   2431 
   2432   if (!hash || !hash[0]) {
   2433     callback(RC_INVALID_STATE, "hash is required", client, callback_userdata);
   2434     return NULL;
   2435   }
   2436 
   2437 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2438   if (client->state.external_client && client->state.external_client->begin_load_game)
   2439     return client->state.external_client->begin_load_game(client, hash, callback, callback_userdata);
   2440 #endif
   2441 
   2442   load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
   2443   if (!load_state) {
   2444     callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
   2445     return NULL;
   2446   }
   2447 
   2448   load_state->client = client;
   2449   load_state->callback = callback;
   2450   load_state->callback_userdata = callback_userdata;
   2451 
   2452   return rc_client_load_game(load_state, hash, NULL);
   2453 }
   2454 
   2455 #ifdef RC_CLIENT_SUPPORTS_HASH
   2456 
   2457 rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client)
   2458 {
   2459   if (client && client->state.load)
   2460     return &client->state.load->hash_iterator;
   2461 
   2462   return NULL;
   2463 }
   2464 
   2465 rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client,
   2466     uint32_t console_id, const char* file_path,
   2467     const uint8_t* data, size_t data_size,
   2468     rc_client_callback_t callback, void* callback_userdata)
   2469 {
   2470   rc_client_load_state_t* load_state;
   2471   char hash[33];
   2472 
   2473   if (!client) {
   2474     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
   2475     return NULL;
   2476   }
   2477 
   2478 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2479   /* if a add_game_hash handler exists, do the identification locally, then pass the
   2480    * resulting game_id/hash to the external client */
   2481   if (client->state.external_client && !client->state.external_client->add_game_hash) {
   2482     if (client->state.external_client->begin_identify_and_load_game)
   2483       return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata);
   2484   }
   2485 #endif
   2486 
   2487   if (data) {
   2488     if (file_path) {
   2489       RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path);
   2490     }
   2491     else {
   2492       RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data);
   2493     }
   2494   }
   2495   else if (file_path) {
   2496     RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path);
   2497   }
   2498   else {
   2499     callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata);
   2500     return NULL;
   2501   }
   2502 
   2503   if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) {
   2504     g_hash_client = client;
   2505     rc_hash_init_error_message_callback(rc_client_log_hash_message);
   2506     rc_hash_init_verbose_message_callback(rc_client_log_hash_message);
   2507   }
   2508 
   2509   if (!file_path)
   2510     file_path = "?";
   2511 
   2512   load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
   2513   if (!load_state) {
   2514     callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
   2515     return NULL;
   2516   }
   2517   load_state->client = client;
   2518   load_state->callback = callback;
   2519   load_state->callback_userdata = callback_userdata;
   2520 
   2521   if (console_id == RC_CONSOLE_UNKNOWN) {
   2522     rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size);
   2523 
   2524     if (!rc_hash_iterate(hash, &load_state->hash_iterator)) {
   2525       rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed");
   2526       return NULL;
   2527     }
   2528 
   2529     load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1];
   2530   }
   2531   else {
   2532     /* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */
   2533     load_state->hash_console_id = console_id;
   2534 
   2535     if (data != NULL) {
   2536       if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) {
   2537         rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed");
   2538         return NULL;
   2539       }
   2540     }
   2541     else {
   2542       if (!rc_hash_generate_from_file(hash, console_id, file_path)) {
   2543         rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed");
   2544         return NULL;
   2545       }
   2546     }
   2547   }
   2548 
   2549   return rc_client_load_game(load_state, hash, file_path);
   2550 }
   2551 
   2552 #endif /* RC_CLIENT_SUPPORTS_HASH */
   2553 
   2554 int rc_client_get_load_game_state(const rc_client_t* client)
   2555 {
   2556   int state = RC_CLIENT_LOAD_GAME_STATE_NONE;
   2557   if (client) {
   2558     const rc_client_load_state_t* load_state = client->state.load;
   2559     if (load_state)
   2560       state = load_state->progress;
   2561     else if (client->game)
   2562       state = RC_CLIENT_LOAD_GAME_STATE_DONE;
   2563   }
   2564 
   2565   return state;
   2566 }
   2567 
   2568 int rc_client_is_game_loaded(const rc_client_t* client)
   2569 {
   2570   const rc_client_game_t* game;
   2571 
   2572   if (!client)
   2573     return 0;
   2574 
   2575 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2576   if (client->state.external_client && client->state.external_client->get_game_info)
   2577     game = client->state.external_client->get_game_info();
   2578   else
   2579 #endif
   2580     game = client->game ? &client->game->public_ : NULL;
   2581 
   2582   return (game && game->id != 0);
   2583 }
   2584 
   2585 static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game)
   2586 {
   2587   rc_client_achievement_info_t* achievement;
   2588   rc_client_achievement_info_t* achievement_stop;
   2589   rc_client_leaderboard_info_t* leaderboard;
   2590   rc_client_leaderboard_info_t* leaderboard_stop;
   2591   rc_client_subset_info_t* subset;
   2592 
   2593   for (subset = game->subsets; subset; subset = subset->next) {
   2594     achievement = subset->achievements;
   2595     achievement_stop = achievement + subset->public_.num_achievements;
   2596     for (; achievement < achievement_stop; ++achievement) {
   2597       if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE &&
   2598           achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) {
   2599         achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
   2600         subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
   2601       }
   2602     }
   2603 
   2604     leaderboard = subset->leaderboards;
   2605     leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
   2606     for (; leaderboard < leaderboard_stop; ++leaderboard) {
   2607       if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING)
   2608         rc_client_release_leaderboard_tracker(game, leaderboard);
   2609     }
   2610   }
   2611 
   2612   rc_client_hide_progress_tracker(client, game);
   2613 }
   2614 
   2615 void rc_client_unload_game(rc_client_t* client)
   2616 {
   2617   rc_client_game_info_t* game;
   2618   rc_client_scheduled_callback_data_t** last;
   2619   rc_client_scheduled_callback_data_t* next;
   2620 
   2621   if (!client)
   2622     return;
   2623 
   2624 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2625   if (client->state.external_client && client->state.external_client->unload_game) {
   2626     client->state.external_client->unload_game();
   2627 
   2628     /* a game object may have been allocated to manage hashes */
   2629     game = client->game;
   2630     client->game = NULL;
   2631     if (game != NULL)
   2632       rc_client_free_game(game);
   2633 
   2634     return;
   2635   }
   2636 #endif
   2637 
   2638   rc_mutex_lock(&client->state.mutex);
   2639 
   2640   game = client->game;
   2641   client->game = NULL;
   2642 
   2643   if (client->state.load) {
   2644     /* this mimics rc_client_abort_async without nesting the lock */
   2645     client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED;
   2646     client->state.load = NULL;
   2647   }
   2648 
   2649   if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED)
   2650     client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON;
   2651 
   2652   if (game != NULL)
   2653     rc_client_game_mark_ui_to_be_hidden(client, game);
   2654 
   2655   last = &client->state.scheduled_callbacks;
   2656   do {
   2657     next = *last;
   2658     if (!next)
   2659       break;
   2660 
   2661     /* remove rich presence ping scheduled event for game */
   2662     if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) {
   2663       *last = next->next;
   2664       continue;
   2665     }
   2666 
   2667     last = &next->next;
   2668   } while (1);
   2669 
   2670   rc_mutex_unlock(&client->state.mutex);
   2671 
   2672   if (game != NULL) {
   2673     rc_client_raise_pending_events(client, game);
   2674 
   2675     RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id);
   2676     rc_client_free_game(game);
   2677   }
   2678 }
   2679 
   2680 static void rc_client_change_media_internal(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata)
   2681 {
   2682   client->game->public_.hash = game_hash->hash;
   2683 
   2684   if (game_hash->game_id == client->game->public_.id) {
   2685     RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash);
   2686   }
   2687   else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) {
   2688     RC_CLIENT_LOG_INFO(client, "Switching to unknown media");
   2689   }
   2690   else if (game_hash->game_id == 0) {
   2691     if (client->state.hardcore) {
   2692       RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", game_hash->hash);
   2693       rc_client_set_hardcore_enabled(client, 0);
   2694       callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, callback_userdata);
   2695       return;
   2696     }
   2697 
   2698     RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash);
   2699   }
   2700   else {
   2701     RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash);
   2702   }
   2703 
   2704   callback(RC_OK, NULL, client, callback_userdata);
   2705 }
   2706 
   2707 static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data)
   2708 {
   2709   rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
   2710   rc_client_t* client = load_state->client;
   2711   rc_api_resolve_hash_response_t resolve_hash_response;
   2712 
   2713   int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response);
   2714   const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response);
   2715 
   2716   const int async_aborted = rc_client_end_async(client, &load_state->async_handle);
   2717   if (async_aborted) {
   2718     if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) {
   2719       RC_CLIENT_LOG_VERBOSE(client, "Media change aborted");
   2720       /* if lookup succeeded, still capture the new hash */
   2721       if (result == RC_OK)
   2722         load_state->hash->game_id = resolve_hash_response.game_id;
   2723     }
   2724   }
   2725   else if (client->game != load_state->game) {
   2726     /* loaded game changed. return success regardless of result */
   2727     load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
   2728   }
   2729   else if (error_message) {
   2730     load_state->callback(result, error_message, client, load_state->callback_userdata);
   2731   }
   2732   else {
   2733     load_state->hash->game_id = resolve_hash_response.game_id;
   2734 
   2735     if (resolve_hash_response.game_id != 0) {
   2736       RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash);
   2737     }
   2738 
   2739     rc_client_change_media_internal(client, load_state->hash, load_state->callback, load_state->callback_userdata);
   2740   }
   2741 
   2742   free(load_state);
   2743   rc_api_destroy_resolve_hash_response(&resolve_hash_response);
   2744 }
   2745 
   2746 static rc_client_async_handle_t* rc_client_begin_change_media_internal(rc_client_t* client,
   2747     rc_client_game_info_t* game, rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata)
   2748 {
   2749   rc_client_load_state_t* callback_data;
   2750   rc_client_async_handle_t* async_handle;
   2751   rc_api_resolve_hash_request_t resolve_hash_request;
   2752   rc_api_request_t request;
   2753   int result;
   2754 
   2755   if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) {
   2756     rc_client_change_media_internal(client, game_hash, callback, callback_userdata);
   2757     return NULL;
   2758   }
   2759 
   2760   /* call the server to make sure the hash is valid for the loaded game */
   2761   memset(&resolve_hash_request, 0, sizeof(resolve_hash_request));
   2762   resolve_hash_request.game_hash = game_hash->hash;
   2763 
   2764   result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request);
   2765   if (result != RC_OK) {
   2766     callback(result, rc_error_str(result), client, callback_userdata);
   2767     return NULL;
   2768   }
   2769 
   2770   callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t));
   2771   if (!callback_data) {
   2772     callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
   2773     return NULL;
   2774   }
   2775 
   2776   callback_data->callback = callback;
   2777   callback_data->callback_userdata = callback_userdata;
   2778   callback_data->client = client;
   2779   callback_data->hash = game_hash;
   2780   callback_data->game = game;
   2781 
   2782   async_handle = &callback_data->async_handle;
   2783   rc_client_begin_async(client, async_handle);
   2784   client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client);
   2785 
   2786   rc_api_destroy_request(&request);
   2787 
   2788   /* if handle is no longer valid, the async operation completed synchronously */
   2789   return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL;
   2790 }
   2791 
   2792 static rc_client_game_info_t* rc_client_check_pending_media(rc_client_t* client, const rc_client_pending_media_t* media)
   2793 {
   2794   rc_client_game_info_t* game;
   2795   rc_client_pending_media_t* pending_media = NULL;
   2796 
   2797   rc_mutex_lock(&client->state.mutex);
   2798   if (client->state.load) {
   2799     game = client->state.load->game;
   2800     if (!game || game->public_.console_id == 0) {
   2801       /* still waiting for game data */
   2802       pending_media = client->state.load->pending_media;
   2803       if (pending_media)
   2804         rc_client_free_pending_media(pending_media);
   2805 
   2806       pending_media = (rc_client_pending_media_t*)malloc(sizeof(*pending_media));
   2807       if (!pending_media) {
   2808         rc_mutex_unlock(&client->state.mutex);
   2809         media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata);
   2810         return NULL;
   2811       }
   2812 
   2813       memcpy(pending_media, media, sizeof(*pending_media));
   2814       if (media->hash)
   2815         pending_media->hash = strdup(media->hash);
   2816 
   2817 #ifdef RC_CLIENT_SUPPORTS_HASH
   2818       if (media->file_path)
   2819         pending_media->file_path = strdup(media->file_path);
   2820 
   2821       if (media->data && media->data_size) {
   2822         pending_media->data = (uint8_t*)malloc(media->data_size);
   2823         if (!pending_media->data) {
   2824           rc_mutex_unlock(&client->state.mutex);
   2825           media->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, media->callback_userdata);
   2826           return NULL;
   2827         }
   2828         memcpy(pending_media->data, media->data, media->data_size);
   2829       } else {
   2830         pending_media->data = NULL;
   2831       }
   2832 #endif
   2833 
   2834       client->state.load->pending_media = pending_media;
   2835     }
   2836   }
   2837   else {
   2838     game = client->game;
   2839   }
   2840   rc_mutex_unlock(&client->state.mutex);
   2841 
   2842   if (!game) {
   2843     media->callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, media->callback_userdata);
   2844     return NULL;
   2845   }
   2846 
   2847   /* still waiting for game data - don't call callback - it's queued */
   2848   if (pending_media) 
   2849     return NULL;
   2850 
   2851   return game;
   2852 }
   2853 
   2854 #ifdef RC_CLIENT_SUPPORTS_HASH
   2855 
   2856 rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path,
   2857     const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata)
   2858 {
   2859   rc_client_pending_media_t media;
   2860   rc_client_game_hash_t* game_hash = NULL;
   2861   rc_client_game_info_t* game;
   2862   rc_client_media_hash_t* media_hash;
   2863   uint32_t path_djb2;
   2864 
   2865   if (!client) {
   2866     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
   2867     return NULL;
   2868   }
   2869 
   2870   if (!data && !file_path) {
   2871     callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata);
   2872     return NULL;
   2873   }
   2874 
   2875 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2876   if (client->state.external_client && !client->state.external_client->begin_change_media_from_hash) {
   2877     if (client->state.external_client->begin_change_media)
   2878       return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata);
   2879   }
   2880 #endif
   2881 
   2882   memset(&media, 0, sizeof(media));
   2883   media.file_path = file_path;
   2884   media.data = (uint8_t*)data;
   2885   media.data_size = data_size;
   2886   media.callback = callback;
   2887   media.callback_userdata = callback_userdata;
   2888 
   2889   game = rc_client_check_pending_media(client, &media);
   2890   if (game == NULL)
   2891     return NULL;
   2892 
   2893   /* check to see if we've already hashed this file */
   2894   path_djb2 = rc_djb2(file_path);
   2895   rc_mutex_lock(&client->state.mutex);
   2896   for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) {
   2897     if (media_hash->path_djb2 == path_djb2) {
   2898       game_hash = media_hash->game_hash;
   2899       break;
   2900     }
   2901   }
   2902   rc_mutex_unlock(&client->state.mutex);
   2903 
   2904   if (!game_hash) {
   2905     char hash[33];
   2906     int result;
   2907 
   2908     if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) {
   2909       g_hash_client = client;
   2910       rc_hash_init_error_message_callback(rc_client_log_hash_message);
   2911       rc_hash_init_verbose_message_callback(rc_client_log_hash_message);
   2912     }
   2913 
   2914     if (data != NULL)
   2915       result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size);
   2916     else
   2917       result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path);
   2918 
   2919     g_hash_client = NULL;
   2920 
   2921     if (!result) {
   2922       /* when changing discs, if the disc is not supported by the system, allow it. this is
   2923        * primarily for games that support user-provided audio CDs, but does allow using discs
   2924        * from other systems for games that leverage user-provided discs. */
   2925       strcpy_s(hash, sizeof(hash), "[NO HASH]");
   2926     }
   2927 
   2928     game_hash = rc_client_find_game_hash(client, hash);
   2929 
   2930     media_hash = (rc_client_media_hash_t*)rc_buffer_alloc(&game->buffer, sizeof(*media_hash));
   2931     media_hash->game_hash = game_hash;
   2932     media_hash->path_djb2 = path_djb2;
   2933 
   2934     rc_mutex_lock(&client->state.mutex);
   2935     media_hash->next = game->media_hash;
   2936     game->media_hash = media_hash;
   2937     rc_mutex_unlock(&client->state.mutex);
   2938 
   2939     if (!result) {
   2940 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2941       if (client->state.external_client && client->state.external_client->begin_change_media_from_hash)
   2942         return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata);
   2943 #endif
   2944 
   2945       rc_client_change_media_internal(client, game_hash, callback, callback_userdata);
   2946       return NULL;
   2947     }
   2948   }
   2949 
   2950 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2951   if (client->state.external_client) {
   2952     if (client->state.external_client->add_game_hash)
   2953       client->state.external_client->add_game_hash(game_hash->hash, game_hash->game_id);
   2954     if (client->state.external_client->begin_change_media_from_hash)
   2955       return client->state.external_client->begin_change_media_from_hash(client, game_hash->hash, callback, callback_userdata);
   2956   }
   2957 #endif
   2958 
   2959   return rc_client_begin_change_media_internal(client, game, game_hash, callback, callback_userdata);
   2960 }
   2961 
   2962 #endif /* RC_CLIENT_SUPPORTS_HASH */
   2963 
   2964 rc_client_async_handle_t* rc_client_begin_change_media_from_hash(rc_client_t* client, const char* hash,
   2965     rc_client_callback_t callback, void* callback_userdata)
   2966 {
   2967   rc_client_pending_media_t media;
   2968   rc_client_game_hash_t* game_hash;
   2969   rc_client_game_info_t* game;
   2970 
   2971   if (!client) {
   2972     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
   2973     return NULL;
   2974   }
   2975 
   2976   if (!hash || !hash[0]) {
   2977     callback(RC_INVALID_STATE, "hash is required", client, callback_userdata);
   2978     return NULL;
   2979   }
   2980 
   2981 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   2982   if (client->state.external_client && client->state.external_client->begin_change_media_from_hash) {
   2983     return client->state.external_client->begin_change_media_from_hash(client, hash, callback, callback_userdata);
   2984   }
   2985 #endif
   2986 
   2987   memset(&media, 0, sizeof(media));
   2988   media.hash = hash;
   2989   media.callback = callback;
   2990   media.callback_userdata = callback_userdata;
   2991 
   2992   game = rc_client_check_pending_media(client, &media);
   2993   if (game == NULL)
   2994     return NULL;
   2995 
   2996   /* check to see if we've already hashed this file. */
   2997   game_hash = rc_client_find_game_hash(client, hash);
   2998   return rc_client_begin_change_media_internal(client, game, game_hash, callback, callback_userdata);
   2999 }
   3000 
   3001 const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client)
   3002 {
   3003   if (!client)
   3004     return NULL;
   3005 
   3006 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3007   if (client->state.external_client && client->state.external_client->get_game_info)
   3008     return client->state.external_client->get_game_info();
   3009 #endif
   3010 
   3011   return client->game ? &client->game->public_ : NULL;
   3012 }
   3013 
   3014 int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size)
   3015 {
   3016   if (!game)
   3017     return RC_INVALID_STATE;
   3018 
   3019   return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name);
   3020 }
   3021 
   3022 /* ===== Subsets ===== */
   3023 
   3024 rc_client_async_handle_t* rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_client_callback_t callback, void* callback_userdata)
   3025 {
   3026   char buffer[32];
   3027   rc_client_load_state_t* load_state;
   3028 
   3029   if (!client) {
   3030     callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
   3031     return NULL;
   3032   }
   3033 
   3034 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3035   if (client->state.external_client && client->state.external_client->begin_load_subset)
   3036     return client->state.external_client->begin_load_subset(client, subset_id, callback, callback_userdata);
   3037 #endif
   3038 
   3039   if (!rc_client_is_game_loaded(client)) {
   3040     callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata);
   3041     return NULL;
   3042   }
   3043 
   3044   snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)subset_id);
   3045 
   3046   load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
   3047   if (!load_state) {
   3048     callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
   3049     return NULL;
   3050   }
   3051 
   3052   load_state->client = client;
   3053   load_state->callback = callback;
   3054   load_state->callback_userdata = callback_userdata;
   3055   load_state->game = client->game;
   3056   load_state->hash = rc_client_find_game_hash(client, buffer);
   3057   load_state->hash->game_id = subset_id;
   3058   client->state.load = load_state;
   3059 
   3060   rc_client_process_resolved_hash(load_state);
   3061 
   3062   return (client->state.load == load_state) ? &load_state->async_handle : NULL;
   3063 }
   3064 
   3065 const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id)
   3066 {
   3067   rc_client_subset_info_t* subset;
   3068 
   3069   if (!client)
   3070     return NULL;
   3071 
   3072 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3073   if (client->state.external_client && client->state.external_client->get_subset_info)
   3074     return client->state.external_client->get_subset_info(subset_id);
   3075 #endif
   3076 
   3077   if (!client->game)
   3078     return NULL;
   3079 
   3080   for (subset = client->game->subsets; subset; subset = subset->next) {
   3081     if (subset->public_.id == subset_id)
   3082       return &subset->public_;
   3083   }
   3084 
   3085   return NULL;
   3086 }
   3087 
   3088 /* ===== Achievements ===== */
   3089 
   3090 static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time)
   3091 {
   3092   uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN;
   3093   uint32_t new_measured_value = 0;
   3094 
   3095   if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED)
   3096     return;
   3097 
   3098   achievement->public_.measured_progress[0] = '\0';
   3099 
   3100   if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) {
   3101     /* achievement unlocked */
   3102     if (achievement->public_.unlock_time >= recent_unlock_time) {
   3103       new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED;
   3104     } else {
   3105       new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED;
   3106 
   3107       if (client->state.disconnect && rc_client_is_award_achievement_pending(client, achievement->public_.id))
   3108         new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED;
   3109     }
   3110   }
   3111   else {
   3112     /* active achievement */
   3113     new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ?
   3114         RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED;
   3115 
   3116     if (achievement->trigger) {
   3117       if (achievement->trigger->measured_target) {
   3118         if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) {
   3119           /* value hasn't been initialized yet, leave progress string empty */
   3120         }
   3121         else if (achievement->trigger->measured_value == 0) {
   3122           /* value is 0, leave progress string empty. update progress to 0.0 */
   3123           achievement->public_.measured_percent = 0.0;
   3124         }
   3125         else {
   3126           /* clamp measured value at target (can't get more than 100%) */
   3127           new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ?
   3128               achievement->trigger->measured_target : achievement->trigger->measured_value;
   3129 
   3130           achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target;
   3131 
   3132           if (!achievement->trigger->measured_as_percent) {
   3133             snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress),
   3134                 "%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target);
   3135           }
   3136           else if (achievement->public_.measured_percent >= 1.0) {
   3137             snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress),
   3138                 "%lu%%", (unsigned long)achievement->public_.measured_percent);
   3139           }
   3140         }
   3141       }
   3142 
   3143       if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED)
   3144         new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE;
   3145       else if (achievement->public_.measured_percent >= 80.0)
   3146         new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE;
   3147     }
   3148   }
   3149 
   3150   achievement->public_.bucket = new_bucket;
   3151 }
   3152 
   3153 static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type)
   3154 {
   3155   switch (bucket_type) {
   3156     case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked";
   3157     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked";
   3158     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported";
   3159     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial";
   3160     case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked";
   3161     case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges";
   3162     case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There";
   3163     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: return "Unlocks Not Synced to Server";
   3164     default: return "Unknown";
   3165   }
   3166 }
   3167 
   3168 static const char* rc_client_get_subset_achievement_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset)
   3169 {
   3170   const char** ptr;
   3171   const char* label;
   3172   char* new_label;
   3173   size_t new_label_len;
   3174 
   3175   switch (bucket_type) {
   3176     case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break;
   3177     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break;
   3178     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break;
   3179     case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break;
   3180     default: return rc_client_get_achievement_bucket_label(bucket_type);
   3181   }
   3182 
   3183   if (*ptr)
   3184     return *ptr;
   3185 
   3186   label = rc_client_get_achievement_bucket_label(bucket_type);
   3187   new_label_len = strlen(subset->public_.title) + strlen(label) + 4;
   3188   new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len);
   3189   snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label);
   3190 
   3191   *ptr = new_label;
   3192   return new_label;
   3193 }
   3194 
   3195 static int rc_client_compare_achievement_unlock_times(const void* a, const void* b)
   3196 {
   3197   const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a;
   3198   const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b;
   3199   if (unlock_b->unlock_time == unlock_a->unlock_time)
   3200     return 0;
   3201   return (unlock_b->unlock_time < unlock_a->unlock_time) ? -1 : 1;
   3202 }
   3203 
   3204 static int rc_client_compare_achievement_progress(const void* a, const void* b)
   3205 {
   3206   const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a;
   3207   const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b;
   3208   if (unlock_b->measured_percent == unlock_a->measured_percent) {
   3209     if (unlock_a->id == unlock_b->id)
   3210       return 0;
   3211     return (unlock_a->id < unlock_b->id) ? -1 : 1;
   3212   }
   3213   return (unlock_b->measured_percent < unlock_a->measured_percent) ? -1 : 1;
   3214 }
   3215 
   3216 static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping)
   3217 {
   3218   if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) {
   3219     switch (bucket) {
   3220       case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED:
   3221       case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED:
   3222         return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED;
   3223 
   3224       case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE:
   3225       case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE:
   3226         return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED;
   3227 
   3228       default:
   3229         return bucket;
   3230     }
   3231   }
   3232 
   3233   return bucket;
   3234 }
   3235 
   3236 rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping)
   3237 {
   3238   rc_client_achievement_info_t* achievement;
   3239   rc_client_achievement_info_t* stop;
   3240   rc_client_achievement_t** bucket_achievements;
   3241   rc_client_achievement_t** achievement_ptr;
   3242   rc_client_achievement_bucket_t* bucket_ptr;
   3243   rc_client_achievement_list_info_t* list;
   3244   rc_client_subset_info_t* subset;
   3245   const uint32_t list_size = RC_ALIGN(sizeof(*list));
   3246   uint32_t bucket_counts[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS];
   3247   uint32_t num_buckets;
   3248   uint32_t num_achievements;
   3249   size_t buckets_size;
   3250   uint8_t bucket_type;
   3251   uint32_t num_subsets = 0;
   3252   uint32_t i, j;
   3253   const uint8_t shared_bucket_order[] = {
   3254     RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE,
   3255     RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED,
   3256     RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE,
   3257     RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED,
   3258   };
   3259   const uint8_t subset_bucket_order[] = {
   3260     RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED,
   3261     RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL,
   3262     RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED,
   3263     RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED
   3264   };
   3265   const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS;
   3266 
   3267   if (!client)
   3268     return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t));
   3269 
   3270 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3271   if (client->state.external_client && client->state.external_client->create_achievement_list)
   3272     return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping);
   3273 #endif
   3274 
   3275   if (!client->game)
   3276     return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t));
   3277 
   3278   memset(&bucket_counts, 0, sizeof(bucket_counts));
   3279 
   3280   rc_mutex_lock(&client->state.mutex);
   3281 
   3282   subset = client->game->subsets;
   3283   for (; subset; subset = subset->next) {
   3284     if (!subset->active)
   3285       continue;
   3286 
   3287     num_subsets++;
   3288     achievement = subset->achievements;
   3289     stop = achievement + subset->public_.num_achievements;
   3290     for (; achievement < stop; ++achievement) {
   3291       if (achievement->public_.category & category) {
   3292         rc_client_update_achievement_display_information(client, achievement, recent_unlock_time);
   3293         bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++;
   3294       }
   3295     }
   3296   }
   3297 
   3298   num_buckets = 0;
   3299   num_achievements = 0;
   3300   for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) {
   3301     if (bucket_counts[i]) {
   3302       int needs_split = 0;
   3303 
   3304       num_achievements += bucket_counts[i];
   3305 
   3306       if (num_subsets > 1) {
   3307         for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) {
   3308           if (subset_bucket_order[j] == i) {
   3309             needs_split = 1;
   3310             break;
   3311           }
   3312         }
   3313       }
   3314 
   3315       if (!needs_split) {
   3316         ++num_buckets;
   3317         continue;
   3318       }
   3319 
   3320       subset = client->game->subsets;
   3321       for (; subset; subset = subset->next) {
   3322         if (!subset->active)
   3323           continue;
   3324 
   3325         achievement = subset->achievements;
   3326         stop = achievement + subset->public_.num_achievements;
   3327         for (; achievement < stop; ++achievement) {
   3328           if (achievement->public_.category & category) {
   3329             if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) {
   3330               ++num_buckets;
   3331               break;
   3332             }
   3333           }
   3334         }
   3335       }
   3336     }
   3337   }
   3338 
   3339   buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t));
   3340 
   3341   list = (rc_client_achievement_list_info_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*));
   3342   bucket_ptr = list->public_.buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size);
   3343   achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size);
   3344 
   3345   if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) {
   3346     for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) {
   3347       bucket_type = shared_bucket_order[i];
   3348       if (!bucket_counts[bucket_type])
   3349         continue;
   3350 
   3351       bucket_achievements = achievement_ptr;
   3352       for (subset = client->game->subsets; subset; subset = subset->next) {
   3353         if (!subset->active)
   3354           continue;
   3355 
   3356         achievement = subset->achievements;
   3357         stop = achievement + subset->public_.num_achievements;
   3358         for (; achievement < stop; ++achievement) {
   3359           if (achievement->public_.category & category &&
   3360               rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) {
   3361             *achievement_ptr++ = &achievement->public_;
   3362           }
   3363         }
   3364       }
   3365 
   3366       if (achievement_ptr > bucket_achievements) {
   3367         bucket_ptr->achievements = bucket_achievements;
   3368         bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements);
   3369         bucket_ptr->subset_id = 0;
   3370         bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type);
   3371         bucket_ptr->bucket_type = bucket_type;
   3372 
   3373         if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED)
   3374           qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times);
   3375         else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE)
   3376           qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress);
   3377 
   3378         ++bucket_ptr;
   3379       }
   3380     }
   3381   }
   3382 
   3383   for (subset = client->game->subsets; subset; subset = subset->next) {
   3384     if (!subset->active)
   3385       continue;
   3386 
   3387     for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) {
   3388       bucket_type = subset_bucket_order[i];
   3389       if (!bucket_counts[bucket_type])
   3390         continue;
   3391 
   3392       bucket_achievements = achievement_ptr;
   3393 
   3394       achievement = subset->achievements;
   3395       stop = achievement + subset->public_.num_achievements;
   3396       for (; achievement < stop; ++achievement) {
   3397         if (achievement->public_.category & category &&
   3398             rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) {
   3399           *achievement_ptr++ = &achievement->public_;
   3400         }
   3401       }
   3402 
   3403       if (achievement_ptr > bucket_achievements) {
   3404         bucket_ptr->achievements = bucket_achievements;
   3405         bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements);
   3406         bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0;
   3407         bucket_ptr->bucket_type = bucket_type;
   3408 
   3409         if (num_subsets > 1)
   3410           bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset);
   3411         else
   3412           bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type);
   3413 
   3414         ++bucket_ptr;
   3415       }
   3416     }
   3417   }
   3418 
   3419   rc_mutex_unlock(&client->state.mutex);
   3420 
   3421   list->destroy_func = NULL;
   3422   list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets);
   3423   return &list->public_;
   3424 }
   3425 
   3426 void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list)
   3427 {
   3428   rc_client_achievement_list_info_t* info = (rc_client_achievement_list_info_t*)list;
   3429   if (info->destroy_func)
   3430     info->destroy_func(info);
   3431   else
   3432     free(list);
   3433 }
   3434 
   3435 int rc_client_has_achievements(rc_client_t* client)
   3436 {
   3437   rc_client_subset_info_t* subset;
   3438   int result;
   3439 
   3440   if (!client)
   3441     return 0;
   3442 
   3443 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3444   if (client->state.external_client && client->state.external_client->has_achievements)
   3445     return client->state.external_client->has_achievements();
   3446 #endif
   3447 
   3448   if (!client->game)
   3449     return 0;
   3450 
   3451   rc_mutex_lock(&client->state.mutex);
   3452 
   3453   subset = client->game->subsets;
   3454   result = 0;
   3455   for (; subset; subset = subset->next)
   3456   {
   3457     if (!subset->active)
   3458       continue;
   3459 
   3460     if (subset->public_.num_achievements > 0) {
   3461       result = 1;
   3462       break;
   3463     }
   3464   }
   3465 
   3466   rc_mutex_unlock(&client->state.mutex);
   3467 
   3468   return result;
   3469 }
   3470 
   3471 static const rc_client_achievement_t* rc_client_subset_get_achievement_info(
   3472     rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id)
   3473 {
   3474   rc_client_achievement_info_t* achievement = subset->achievements;
   3475   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   3476 
   3477   for (; achievement < stop; ++achievement) {
   3478     if (achievement->public_.id == id) {
   3479       const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS;
   3480       rc_mutex_lock((rc_mutex_t*)(&client->state.mutex));
   3481       rc_client_update_achievement_display_information(client, achievement, recent_unlock_time);
   3482       rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex));
   3483       return &achievement->public_;
   3484     }
   3485   }
   3486 
   3487   return NULL;
   3488 }
   3489 
   3490 const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id)
   3491 {
   3492   rc_client_subset_info_t* subset;
   3493 
   3494   if (!client)
   3495     return NULL;
   3496 
   3497 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3498   if (client->state.external_client && client->state.external_client->get_achievement_info)
   3499     return client->state.external_client->get_achievement_info(id);
   3500 #endif
   3501 
   3502   if (!client->game)
   3503     return NULL;
   3504 
   3505   for (subset = client->game->subsets; subset; subset = subset->next) {
   3506     const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id);
   3507     if (achievement != NULL)
   3508       return achievement;
   3509   }
   3510 
   3511   return NULL;
   3512 }
   3513 
   3514 int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size)
   3515 {
   3516   const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ?
   3517       RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED;
   3518 
   3519   if (!achievement || !achievement->badge_name[0])
   3520     return rc_client_get_image_url(buffer, buffer_size, image_type, "00000");
   3521 
   3522   return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name);
   3523 }
   3524 
   3525 typedef struct rc_client_award_achievement_callback_data_t
   3526 {
   3527   uint32_t id;
   3528   uint32_t retry_count;
   3529   uint8_t hardcore;
   3530   const char* game_hash;
   3531   time_t unlock_time;
   3532   rc_client_t* client;
   3533   rc_client_scheduled_callback_data_t* scheduled_callback_data;
   3534 } rc_client_award_achievement_callback_data_t;
   3535 
   3536 static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id)
   3537 {
   3538   /* assume lock already held */
   3539   rc_client_scheduled_callback_data_t* scheduled_callback = client->state.scheduled_callbacks;
   3540   for (; scheduled_callback; scheduled_callback = scheduled_callback->next)
   3541   {
   3542     if (scheduled_callback->callback == rc_client_award_achievement_retry)
   3543     {
   3544       rc_client_award_achievement_callback_data_t* ach_data =
   3545         (rc_client_award_achievement_callback_data_t*)scheduled_callback->data;
   3546       if (ach_data->id == achievement_id)
   3547         return 1;
   3548     }
   3549   }
   3550 
   3551   return 0;
   3552 }
   3553 
   3554 static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data);
   3555 
   3556 static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
   3557 {
   3558   rc_client_award_achievement_callback_data_t* ach_data =
   3559     (rc_client_award_achievement_callback_data_t*)callback_data->data;
   3560 
   3561   (void)client;
   3562   (void)now;
   3563 
   3564   rc_client_award_achievement_server_call(ach_data);
   3565 }
   3566 
   3567 static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data)
   3568 {
   3569   rc_client_award_achievement_callback_data_t* ach_data =
   3570       (rc_client_award_achievement_callback_data_t*)callback_data;
   3571   rc_api_award_achievement_response_t award_achievement_response;
   3572 
   3573   int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response);
   3574   const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response);
   3575 
   3576   if (error_message) {
   3577     if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) {
   3578       /* actual error from server */
   3579       RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message);
   3580       rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, result, award_achievement_response.response.error_message);
   3581     }
   3582     else if (ach_data->retry_count++ == 0) {
   3583       /* first retry is immediate */
   3584       RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message);
   3585       rc_client_award_achievement_server_call(ach_data);
   3586       return;
   3587     }
   3588     else {
   3589       /* double wait time between each attempt until we hit a maximum delay of two minutes */
   3590       /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/
   3591       const uint32_t delay = (ach_data->retry_count > 8) ? 120 : (1 << (ach_data->retry_count - 2));
   3592       RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay);
   3593 
   3594       if (!ach_data->scheduled_callback_data) {
   3595         ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data));
   3596         if (!ach_data->scheduled_callback_data) {
   3597           RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id);
   3598           rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
   3599           return;
   3600         }
   3601         ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry;
   3602         ach_data->scheduled_callback_data->data = ach_data;
   3603         ach_data->scheduled_callback_data->related_id = ach_data->id;
   3604       }
   3605 
   3606       ach_data->scheduled_callback_data->when =
   3607           ach_data->client->callbacks.get_time_millisecs(ach_data->client) + delay * 1000;
   3608 
   3609       rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data);
   3610 
   3611       rc_client_update_disconnect_state(ach_data->client);
   3612       return;
   3613     }
   3614   }
   3615   else {
   3616     ach_data->client->user.score = award_achievement_response.new_player_score;
   3617     ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore;
   3618 
   3619     if (award_achievement_response.awarded_achievement_id != ach_data->id) {
   3620       RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message);
   3621     }
   3622     else {
   3623       if (award_achievement_response.response.error_message) {
   3624         /* previously unlocked achievements are returned as a success with an error message */
   3625         RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message);
   3626       }
   3627       else if (ach_data->retry_count) {
   3628         RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u",
   3629             ach_data->id, ach_data->retry_count + 1,
   3630             ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore);
   3631       }
   3632       else {
   3633         RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u",
   3634             ach_data->id,
   3635             ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore);
   3636       }
   3637 
   3638       if (award_achievement_response.achievements_remaining == 0) {
   3639         rc_client_subset_info_t* subset;
   3640         for (subset = ach_data->client->game->subsets; subset; subset = subset->next) {
   3641           if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE &&
   3642               rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) {
   3643             if (subset->public_.id == ach_data->client->game->public_.id) {
   3644               RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id,
   3645                 ach_data->client->state.hardcore ? "mastered" : "completed");
   3646               subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING;
   3647             }
   3648             else {
   3649               RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id,
   3650                 ach_data->client->state.hardcore ? "mastered" : "completed");
   3651 
   3652               /* TODO: subset mastery notification */
   3653               subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN;
   3654             }
   3655           }
   3656         }
   3657       }
   3658     }
   3659   }
   3660 
   3661   if (ach_data->retry_count)
   3662     rc_client_update_disconnect_state(ach_data->client);
   3663 
   3664   if (ach_data->scheduled_callback_data)
   3665     free(ach_data->scheduled_callback_data);
   3666   free(ach_data);
   3667 }
   3668 
   3669 static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data)
   3670 { 
   3671   rc_api_award_achievement_request_t api_params;
   3672   rc_api_request_t request;
   3673   int result;
   3674 
   3675   memset(&api_params, 0, sizeof(api_params));
   3676   api_params.username = ach_data->client->user.username;
   3677   api_params.api_token = ach_data->client->user.token;
   3678   api_params.achievement_id = ach_data->id;
   3679   api_params.hardcore = ach_data->hardcore;
   3680   api_params.game_hash = ach_data->game_hash;
   3681 
   3682   result = rc_api_init_award_achievement_request(&request, &api_params);
   3683   if (result != RC_OK) {
   3684     RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result));
   3685     free(ach_data);
   3686     return;
   3687   }
   3688 
   3689   ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client);
   3690 
   3691   rc_api_destroy_request(&request);
   3692 }
   3693 
   3694 static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement)
   3695 {
   3696   rc_client_award_achievement_callback_data_t* callback_data;
   3697 
   3698   rc_mutex_lock(&client->state.mutex);
   3699 
   3700   if (client->state.hardcore) {
   3701     achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL);
   3702     if (achievement->unlock_time_softcore == 0)
   3703       achievement->unlock_time_softcore = achievement->unlock_time_hardcore;
   3704 
   3705     /* adjust score now - will get accurate score back from server */
   3706     client->user.score += achievement->public_.points;
   3707   }
   3708   else {
   3709     achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL);
   3710 
   3711     /* adjust score now - will get accurate score back from server */
   3712     client->user.score_softcore += achievement->public_.points;
   3713   }
   3714 
   3715   achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED;
   3716   achievement->public_.unlocked |= (client->state.hardcore) ?
   3717     RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
   3718 
   3719   rc_mutex_unlock(&client->state.mutex);
   3720 
   3721   if (client->callbacks.can_submit_achievement_unlock &&
   3722       !client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) {
   3723     RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id);
   3724     return;
   3725   }
   3726 
   3727   /* can't unlock unofficial achievements on the server */
   3728   if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) {
   3729     RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title);
   3730     return;
   3731   }
   3732 
   3733   /* don't actually unlock achievements when spectating */
   3734   if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
   3735     RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title);
   3736     return;
   3737   }
   3738 
   3739   callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data));
   3740   if (!callback_data) {
   3741     RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id);
   3742     rc_client_raise_server_error_event(client, "award_achievement", achievement->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
   3743     return;
   3744   }
   3745   callback_data->client = client;
   3746   callback_data->id = achievement->public_.id;
   3747   callback_data->hardcore = client->state.hardcore;
   3748   callback_data->unlock_time = achievement->public_.unlock_time;
   3749 
   3750   if (client->game) /* may be NULL if this gets called while unloading the game (from another thread - events are raised outside the lock) */
   3751     callback_data->game_hash = client->game->public_.hash;
   3752 
   3753   RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title);
   3754   rc_client_award_achievement_server_call(callback_data);
   3755 }
   3756 
   3757 static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset)
   3758 {
   3759   rc_client_achievement_info_t* achievement = subset->achievements;
   3760   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   3761 
   3762   for (; achievement < stop; ++achievement) {
   3763     rc_trigger_t* trigger = achievement->trigger;
   3764     if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
   3765       continue;
   3766 
   3767     if (trigger->state == RC_TRIGGER_STATE_PRIMED) {
   3768       achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
   3769       subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
   3770     }
   3771 
   3772     rc_reset_trigger(trigger);
   3773   }
   3774 }
   3775 
   3776 static void rc_client_reset_achievements(rc_client_t* client)
   3777 {
   3778   rc_client_subset_info_t* subset;
   3779   for (subset = client->game->subsets; subset; subset = subset->next)
   3780     rc_client_subset_reset_achievements(subset);
   3781 }
   3782 
   3783 /* ===== Leaderboards ===== */
   3784 
   3785 static rc_client_leaderboard_info_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id)
   3786 {
   3787   rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
   3788   rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
   3789 
   3790   for (; leaderboard < stop; ++leaderboard) {
   3791     if (leaderboard->public_.id == id)
   3792       return leaderboard;
   3793   }
   3794 
   3795   return NULL;
   3796 }
   3797 
   3798 const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id)
   3799 {
   3800   rc_client_subset_info_t* subset;
   3801 
   3802   if (!client)
   3803     return NULL;
   3804 
   3805 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3806   if (client->state.external_client && client->state.external_client->get_leaderboard_info)
   3807     return client->state.external_client->get_leaderboard_info(id);
   3808 #endif
   3809 
   3810   if (!client->game)
   3811     return NULL;
   3812 
   3813   for (subset = client->game->subsets; subset; subset = subset->next) {
   3814     const rc_client_leaderboard_info_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id);
   3815     if (leaderboard != NULL)
   3816       return &leaderboard->public_;
   3817   }
   3818  
   3819   return NULL;
   3820 }
   3821 
   3822 static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type)
   3823 {
   3824   switch (bucket_type) {
   3825     case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive";
   3826     case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active";
   3827     case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported";
   3828     case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All";
   3829     default: return "Unknown";
   3830   }
   3831 }
   3832 
   3833 static const char* rc_client_get_subset_leaderboard_bucket_label(uint8_t bucket_type, rc_client_game_info_t* game, rc_client_subset_info_t* subset)
   3834 {
   3835   const char** ptr;
   3836   const char* label;
   3837   char* new_label;
   3838   size_t new_label_len;
   3839 
   3840   switch (bucket_type) {
   3841     case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break;
   3842     case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break;
   3843     case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break;
   3844     default: return rc_client_get_achievement_bucket_label(bucket_type);
   3845   }
   3846 
   3847   if (*ptr)
   3848     return *ptr;
   3849 
   3850   label = rc_client_get_leaderboard_bucket_label(bucket_type);
   3851   new_label_len = strlen(subset->public_.title) + strlen(label) + 4;
   3852   new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len);
   3853   snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label);
   3854 
   3855   *ptr = new_label;
   3856   return new_label;
   3857 }
   3858 
   3859 static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping)
   3860 {
   3861   switch (leaderboard->public_.state) {
   3862     case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
   3863       return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ?
   3864         RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE;
   3865 
   3866     case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
   3867       return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED;
   3868 
   3869     default:
   3870       return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ?
   3871         RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE;
   3872   }
   3873 }
   3874 
   3875 rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping)
   3876 {
   3877   rc_client_leaderboard_info_t* leaderboard;
   3878   rc_client_leaderboard_info_t* stop;
   3879   rc_client_leaderboard_t** bucket_leaderboards;
   3880   rc_client_leaderboard_t** leaderboard_ptr;
   3881   rc_client_leaderboard_bucket_t* bucket_ptr;
   3882   rc_client_leaderboard_list_info_t* list;
   3883   rc_client_subset_info_t* subset;
   3884   const uint32_t list_size = RC_ALIGN(sizeof(*list));
   3885   uint32_t bucket_counts[8];
   3886   uint32_t num_buckets;
   3887   uint32_t num_leaderboards;
   3888   size_t buckets_size;
   3889   uint8_t bucket_type;
   3890   uint32_t num_subsets = 0;
   3891   uint32_t i, j;
   3892   const uint8_t shared_bucket_order[] = {
   3893     RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE
   3894   };
   3895   const uint8_t subset_bucket_order[] = {
   3896     RC_CLIENT_LEADERBOARD_BUCKET_ALL,
   3897     RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE,
   3898     RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED
   3899   };
   3900 
   3901   if (!client)
   3902     return calloc(1, sizeof(rc_client_leaderboard_list_t));
   3903 
   3904 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   3905   if (client->state.external_client && client->state.external_client->create_leaderboard_list)
   3906     return (rc_client_leaderboard_list_t*)client->state.external_client->create_leaderboard_list(grouping);
   3907 #endif
   3908 
   3909   if (!client->game)
   3910     return calloc(1, sizeof(rc_client_leaderboard_list_t));
   3911 
   3912   memset(&bucket_counts, 0, sizeof(bucket_counts));
   3913 
   3914   rc_mutex_lock(&client->state.mutex);
   3915 
   3916   subset = client->game->subsets;
   3917   for (; subset; subset = subset->next) {
   3918     if (!subset->active)
   3919       continue;
   3920 
   3921     num_subsets++;
   3922     leaderboard = subset->leaderboards;
   3923     stop = leaderboard + subset->public_.num_leaderboards;
   3924     for (; leaderboard < stop; ++leaderboard) {
   3925       if (leaderboard->hidden)
   3926         continue;
   3927 
   3928       leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping);
   3929       bucket_counts[leaderboard->bucket]++;
   3930     }
   3931   }
   3932 
   3933   num_buckets = 0;
   3934   num_leaderboards = 0;
   3935   for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) {
   3936     if (bucket_counts[i]) {
   3937       int needs_split = 0;
   3938 
   3939       num_leaderboards += bucket_counts[i];
   3940 
   3941       if (num_subsets > 1) {
   3942         for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) {
   3943           if (subset_bucket_order[j] == i) {
   3944             needs_split = 1;
   3945             break;
   3946           }
   3947         }
   3948       }
   3949 
   3950       if (!needs_split) {
   3951         ++num_buckets;
   3952         continue;
   3953       }
   3954 
   3955       subset = client->game->subsets;
   3956       for (; subset; subset = subset->next) {
   3957         if (!subset->active)
   3958           continue;
   3959 
   3960         leaderboard = subset->leaderboards;
   3961         stop = leaderboard + subset->public_.num_leaderboards;
   3962         for (; leaderboard < stop; ++leaderboard) {
   3963           if (leaderboard->bucket == i) {
   3964             ++num_buckets;
   3965             break;
   3966           }
   3967         }
   3968       }
   3969     }
   3970   }
   3971 
   3972   buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t));
   3973 
   3974   list = (rc_client_leaderboard_list_info_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*));
   3975   bucket_ptr = list->public_.buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size);
   3976   leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size);
   3977 
   3978   if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) {
   3979     for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) {
   3980       bucket_type = shared_bucket_order[i];
   3981       if (!bucket_counts[bucket_type])
   3982         continue;
   3983 
   3984       bucket_leaderboards = leaderboard_ptr;
   3985       for (subset = client->game->subsets; subset; subset = subset->next) {
   3986         if (!subset->active)
   3987           continue;
   3988 
   3989         leaderboard = subset->leaderboards;
   3990         stop = leaderboard + subset->public_.num_leaderboards;
   3991         for (; leaderboard < stop; ++leaderboard) {
   3992           if (leaderboard->bucket == bucket_type && !leaderboard->hidden)
   3993             *leaderboard_ptr++ = &leaderboard->public_;
   3994         }
   3995       }
   3996 
   3997       if (leaderboard_ptr > bucket_leaderboards) {
   3998         bucket_ptr->leaderboards = bucket_leaderboards;
   3999         bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards);
   4000         bucket_ptr->subset_id = 0;
   4001         bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type);
   4002         bucket_ptr->bucket_type = bucket_type;
   4003         ++bucket_ptr;
   4004       }
   4005     }
   4006   }
   4007 
   4008   for (subset = client->game->subsets; subset; subset = subset->next) {
   4009     if (!subset->active)
   4010       continue;
   4011 
   4012     for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) {
   4013       bucket_type = subset_bucket_order[i];
   4014       if (!bucket_counts[bucket_type])
   4015         continue;
   4016 
   4017       bucket_leaderboards = leaderboard_ptr;
   4018 
   4019       leaderboard = subset->leaderboards;
   4020       stop = leaderboard + subset->public_.num_leaderboards;
   4021       for (; leaderboard < stop; ++leaderboard) {
   4022         if (leaderboard->bucket == bucket_type && !leaderboard->hidden)
   4023           *leaderboard_ptr++ = &leaderboard->public_;
   4024       }
   4025 
   4026       if (leaderboard_ptr > bucket_leaderboards) {
   4027         bucket_ptr->leaderboards = bucket_leaderboards;
   4028         bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards);
   4029         bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0;
   4030         bucket_ptr->bucket_type = bucket_type;
   4031 
   4032         if (num_subsets > 1)
   4033           bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset);
   4034         else
   4035           bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type);
   4036 
   4037         ++bucket_ptr;
   4038       }
   4039     }
   4040   }
   4041 
   4042   rc_mutex_unlock(&client->state.mutex);
   4043 
   4044   list->destroy_func = NULL;
   4045   list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets);
   4046   return &list->public_;
   4047 }
   4048 
   4049 void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list)
   4050 {
   4051   rc_client_leaderboard_list_info_t* info = (rc_client_leaderboard_list_info_t*)list;
   4052   if (info->destroy_func)
   4053     info->destroy_func(info);
   4054   else
   4055     free(list);
   4056 }
   4057 
   4058 int rc_client_has_leaderboards(rc_client_t* client)
   4059 {
   4060   rc_client_subset_info_t* subset;
   4061   int result;
   4062 
   4063   if (!client)
   4064     return 0;
   4065 
   4066 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4067   if (client->state.external_client && client->state.external_client->has_leaderboards)
   4068     return client->state.external_client->has_leaderboards();
   4069 #endif
   4070 
   4071   if (!client->game)
   4072     return 0;
   4073 
   4074   rc_mutex_lock(&client->state.mutex);
   4075 
   4076   subset = client->game->subsets;
   4077   result = 0;
   4078   for (; subset; subset = subset->next)
   4079   {
   4080     if (!subset->active)
   4081       continue;
   4082 
   4083     if (subset->public_.num_leaderboards > 0) {
   4084       result = 1;
   4085       break;
   4086     }
   4087   }
   4088 
   4089   rc_mutex_unlock(&client->state.mutex);
   4090 
   4091   return result;
   4092 }
   4093 
   4094 void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
   4095 {
   4096   rc_client_leaderboard_tracker_info_t* tracker;
   4097   rc_client_leaderboard_tracker_info_t* available_tracker = NULL;
   4098 
   4099   for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) {
   4100     if (tracker->reference_count == 0) {
   4101       if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE)
   4102         available_tracker = tracker;
   4103 
   4104       continue;
   4105     }
   4106 
   4107     if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format)
   4108       continue;
   4109 
   4110     if (tracker->raw_value != leaderboard->value) {
   4111       /* if the value comes from tracking hits, we can't assume the trackers started in the
   4112        * same frame, so we can't share the tracker */
   4113       if (tracker->value_from_hits)
   4114         continue;
   4115 
   4116       /* value has changed. prepare an update event */
   4117       tracker->raw_value = leaderboard->value;
   4118       tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE;
   4119       game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
   4120     }
   4121 
   4122     /* attach to the existing tracker */
   4123     ++tracker->reference_count;
   4124     tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE;
   4125     leaderboard->tracker = tracker;
   4126     leaderboard->public_.tracker_value = tracker->public_.display;
   4127     return;
   4128   }
   4129 
   4130   if (!available_tracker) {
   4131     rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers;
   4132 
   4133     available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buffer_alloc(&game->buffer, sizeof(*available_tracker));
   4134     memset(available_tracker, 0, sizeof(*available_tracker));
   4135     available_tracker->public_.id = 1;
   4136 
   4137     for (tracker = *next; tracker; next = &tracker->next, tracker = *next)
   4138       available_tracker->public_.id++;
   4139 
   4140     *next = available_tracker;
   4141   }
   4142 
   4143   /* update the claimed tracker */
   4144   available_tracker->reference_count = 1;
   4145   available_tracker->value_djb2 = leaderboard->value_djb2;
   4146   available_tracker->format = leaderboard->format;
   4147   available_tracker->raw_value = leaderboard->value;
   4148   available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW;
   4149   available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value);
   4150   leaderboard->tracker = available_tracker;
   4151   leaderboard->public_.tracker_value = available_tracker->public_.display;
   4152   game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
   4153 }
   4154 
   4155 void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
   4156 {
   4157   rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker;
   4158   leaderboard->tracker = NULL;
   4159 
   4160   if (tracker && --tracker->reference_count == 0) {
   4161     tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE;
   4162     game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
   4163   }
   4164 }
   4165 
   4166 static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
   4167 {
   4168   rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker;
   4169   if (tracker && tracker->raw_value != leaderboard->value) {
   4170     tracker->raw_value = leaderboard->value;
   4171     tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE;
   4172     game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
   4173   }
   4174 }
   4175 
   4176 typedef struct rc_client_submit_leaderboard_entry_callback_data_t
   4177 {
   4178   uint32_t id;
   4179   int32_t score;
   4180   uint32_t retry_count;
   4181   const char* game_hash;
   4182   time_t submit_time;
   4183   rc_client_t* client;
   4184   rc_client_scheduled_callback_data_t* scheduled_callback_data;
   4185 } rc_client_submit_leaderboard_entry_callback_data_t;
   4186 
   4187 static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data);
   4188 
   4189 static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
   4190 {
   4191   rc_client_submit_leaderboard_entry_callback_data_t* lboard_data =
   4192       (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data;
   4193 
   4194   (void)client;
   4195   (void)now;
   4196 
   4197   rc_client_submit_leaderboard_entry_server_call(lboard_data);
   4198 }
   4199 
   4200 static void rc_client_raise_scoreboard_event(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data,
   4201     const rc_api_submit_lboard_entry_response_t* response)
   4202 {
   4203   rc_client_leaderboard_scoreboard_t sboard;
   4204   rc_client_event_t client_event;
   4205   rc_client_subset_info_t* subset;
   4206   rc_client_t* client = lboard_data->client;
   4207   rc_client_leaderboard_info_t* leaderboard = NULL;
   4208 
   4209   if (!client || !client->game)
   4210     return;
   4211 
   4212   for (subset = client->game->subsets; subset; subset = subset->next) {
   4213     leaderboard = rc_client_subset_get_leaderboard_info(subset, lboard_data->id);
   4214     if (leaderboard != NULL)
   4215       break;
   4216   }
   4217   if (leaderboard == NULL) {
   4218     RC_CLIENT_LOG_ERR_FORMATTED(client, "Trying to raise scoreboard for unknown leaderboard %u", lboard_data->id);
   4219     return;
   4220   }
   4221 
   4222   memset(&sboard, 0, sizeof(sboard));
   4223   sboard.leaderboard_id = lboard_data->id;
   4224   rc_format_value(sboard.submitted_score, sizeof(sboard.submitted_score), response->submitted_score, leaderboard->format);
   4225   rc_format_value(sboard.best_score, sizeof(sboard.best_score), response->best_score, leaderboard->format);
   4226   sboard.new_rank = response->new_rank;
   4227   sboard.num_entries = response->num_entries;
   4228   sboard.num_top_entries = response->num_top_entries;
   4229   if (sboard.num_top_entries > 0) {
   4230     sboard.top_entries = (rc_client_leaderboard_scoreboard_entry_t*)calloc(
   4231       response->num_top_entries, sizeof(rc_client_leaderboard_scoreboard_entry_t));
   4232     if (sboard.top_entries != NULL) {
   4233       uint32_t i;
   4234       for (i = 0; i < response->num_top_entries; i++) {
   4235         sboard.top_entries[i].username = response->top_entries[i].username;
   4236         sboard.top_entries[i].rank = response->top_entries[i].rank;
   4237         rc_format_value(sboard.top_entries[i].score, sizeof(sboard.top_entries[i].score), response->top_entries[i].score,
   4238             leaderboard->format);
   4239       }
   4240     }
   4241   }
   4242 
   4243   memset(&client_event, 0, sizeof(client_event));
   4244   client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD;
   4245   client_event.leaderboard = &leaderboard->public_;
   4246   client_event.leaderboard_scoreboard = &sboard;
   4247 
   4248   lboard_data->client->callbacks.event_handler(&client_event, lboard_data->client);
   4249 
   4250   if (sboard.top_entries != NULL) {
   4251     free(sboard.top_entries);
   4252   }
   4253 }
   4254 
   4255 static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data)
   4256 {
   4257   rc_client_submit_leaderboard_entry_callback_data_t* lboard_data =
   4258       (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data;
   4259   rc_api_submit_lboard_entry_response_t submit_lboard_entry_response;
   4260 
   4261   int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response);
   4262   const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response);
   4263 
   4264   if (error_message) {
   4265     if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) {
   4266       /* actual error from server */
   4267       RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message);
   4268       rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, result, submit_lboard_entry_response.response.error_message);
   4269     }
   4270     else if (lboard_data->retry_count++ == 0) {
   4271       /* first retry is immediate */
   4272       RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message);
   4273       rc_client_submit_leaderboard_entry_server_call(lboard_data);
   4274       return;
   4275     }
   4276     else {
   4277       /* double wait time between each attempt until we hit a maximum delay of two minutes */
   4278       /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/
   4279       const uint32_t delay = (lboard_data->retry_count > 8) ? 120 : (1 << (lboard_data->retry_count - 2));
   4280       RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay);
   4281 
   4282       if (!lboard_data->scheduled_callback_data) {
   4283         lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data));
   4284         if (!lboard_data->scheduled_callback_data) {
   4285           RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Failed to allocate scheduled callback data for reattempt to submit entry for leaderboard %u", lboard_data->id);
   4286           rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
   4287           return;
   4288         }
   4289         lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry;
   4290         lboard_data->scheduled_callback_data->data = lboard_data;
   4291         lboard_data->scheduled_callback_data->related_id = lboard_data->id;
   4292       }
   4293 
   4294       lboard_data->scheduled_callback_data->when =
   4295           lboard_data->client->callbacks.get_time_millisecs(lboard_data->client) + delay * 1000;
   4296 
   4297       rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data);
   4298 
   4299       rc_client_update_disconnect_state(lboard_data->client);
   4300       return;
   4301     }
   4302   }
   4303   else {
   4304     /* raise event for scoreboard */
   4305     if (lboard_data->retry_count < 2) {
   4306       rc_client_raise_scoreboard_event(lboard_data, &submit_lboard_entry_response);
   4307     }
   4308 
   4309     /* not currently doing anything with the response */
   4310     if (lboard_data->retry_count) {
   4311       RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts",
   4312           lboard_data->id, lboard_data->score, lboard_data->retry_count);
   4313     }
   4314   }
   4315 
   4316   if (lboard_data->retry_count)
   4317     rc_client_update_disconnect_state(lboard_data->client);
   4318 
   4319   if (lboard_data->scheduled_callback_data)
   4320     free(lboard_data->scheduled_callback_data);
   4321   free(lboard_data);
   4322 }
   4323 
   4324 static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data)
   4325 {
   4326   rc_api_submit_lboard_entry_request_t api_params;
   4327   rc_api_request_t request;
   4328   int result;
   4329 
   4330   memset(&api_params, 0, sizeof(api_params));
   4331   api_params.username = lboard_data->client->user.username;
   4332   api_params.api_token = lboard_data->client->user.token;
   4333   api_params.leaderboard_id = lboard_data->id;
   4334   api_params.score = lboard_data->score;
   4335   api_params.game_hash = lboard_data->game_hash;
   4336 
   4337   result = rc_api_init_submit_lboard_entry_request(&request, &api_params);
   4338   if (result != RC_OK) {
   4339     RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result));
   4340     return;
   4341   }
   4342 
   4343   lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client);
   4344 
   4345   rc_api_destroy_request(&request);
   4346 }
   4347 
   4348 static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard)
   4349 {
   4350   rc_client_submit_leaderboard_entry_callback_data_t* callback_data;
   4351 
   4352   if (!client->state.hardcore) {
   4353     RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission not allowed in softcore", leaderboard->public_.id);
   4354     return;
   4355   }
   4356 
   4357   if (client->callbacks.can_submit_leaderboard_entry &&
   4358       !client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) {
   4359     RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id);
   4360     return;
   4361   }
   4362 
   4363   /* don't actually submit leaderboard entries when spectating */
   4364   if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
   4365     RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s",
   4366         leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title);
   4367     return;
   4368   }
   4369 
   4370   callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data));
   4371   if (!callback_data) {
   4372     RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id);
   4373     rc_client_raise_server_error_event(client, "submit_lboard_entry", leaderboard->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
   4374     return;
   4375   }
   4376   callback_data->client = client;
   4377   callback_data->id = leaderboard->public_.id;
   4378   callback_data->score = leaderboard->value;
   4379   callback_data->game_hash = client->game->public_.hash;
   4380   callback_data->submit_time = time(NULL);
   4381 
   4382   RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s",
   4383       leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title);
   4384   rc_client_submit_leaderboard_entry_server_call(callback_data);
   4385 }
   4386 
   4387 static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset)
   4388 {
   4389   rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
   4390   rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
   4391 
   4392   for (; leaderboard < stop; ++leaderboard) {
   4393     rc_lboard_t* lboard = leaderboard->lboard;
   4394     if (!lboard)
   4395       continue;
   4396 
   4397     switch (leaderboard->public_.state) {
   4398       case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
   4399       case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
   4400         continue;
   4401 
   4402       case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
   4403         rc_client_release_leaderboard_tracker(game, leaderboard);
   4404         /* fallthrough */ /* to default */
   4405       default:
   4406         leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
   4407         rc_reset_lboard(lboard);
   4408         break;
   4409     }
   4410   }
   4411 }
   4412 
   4413 static void rc_client_reset_leaderboards(rc_client_t* client)
   4414 {
   4415   rc_client_subset_info_t* subset;
   4416   for (subset = client->game->subsets; subset; subset = subset->next)
   4417     rc_client_subset_reset_leaderboards(client->game, subset);
   4418 }
   4419 
   4420 typedef struct rc_client_fetch_leaderboard_entries_callback_data_t {
   4421   rc_client_t* client;
   4422   rc_client_fetch_leaderboard_entries_callback_t callback;
   4423   void* callback_userdata;
   4424   uint32_t leaderboard_id;
   4425   rc_client_async_handle_t async_handle;
   4426 } rc_client_fetch_leaderboard_entries_callback_data_t;
   4427 
   4428 static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data)
   4429 {
   4430   rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data;
   4431   rc_client_t* client = lbinfo_callback_data->client;
   4432   rc_api_fetch_leaderboard_info_response_t lbinfo_response;
   4433   const char* error_message;
   4434   int result;
   4435 
   4436   result = rc_client_end_async(client, &lbinfo_callback_data->async_handle);
   4437   if (result) {
   4438     if (result != RC_CLIENT_ASYNC_DESTROYED) {
   4439       RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted");
   4440     }
   4441     free(lbinfo_callback_data);
   4442     return;
   4443   }
   4444 
   4445   result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response);
   4446   error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response);
   4447   if (error_message) {
   4448     RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message);
   4449     lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata);
   4450   }
   4451   else {
   4452     rc_client_leaderboard_entry_list_info_t* info;
   4453     const size_t list_size = sizeof(*info) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries;
   4454     size_t needed_size = list_size;
   4455     uint32_t i;
   4456 
   4457     for (i = 0; i < lbinfo_response.num_entries; i++)
   4458       needed_size += strlen(lbinfo_response.entries[i].username) + 1;
   4459 
   4460     info = (rc_client_leaderboard_entry_list_info_t*)malloc(needed_size);
   4461     if (!info) {
   4462       lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata);
   4463     }
   4464     else {
   4465       rc_client_leaderboard_entry_list_t* list = &info->public_;
   4466       rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)info + sizeof(*info));
   4467       char* user = (char*)((uint8_t*)list + list_size);
   4468       const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries;
   4469       const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries;
   4470       const size_t logged_in_user_len = strlen(client->user.display_name) + 1;
   4471       info->destroy_func = NULL;
   4472       list->user_index = -1;
   4473 
   4474       for (; lbentry < stop; ++lbentry, ++entry) {
   4475         const size_t len = strlen(lbentry->username) + 1;
   4476         entry->user = user;
   4477         memcpy(user, lbentry->username, len);
   4478         user += len;
   4479 
   4480         if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0)
   4481           list->user_index = (int)(entry - list->entries);
   4482 
   4483         entry->index = lbentry->index;
   4484         entry->rank = lbentry->rank;
   4485         entry->submitted = lbentry->submitted;
   4486 
   4487         rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format);
   4488       }
   4489 
   4490       list->num_entries = lbinfo_response.num_entries;
   4491       list->total_entries = lbinfo_response.total_entries;
   4492 
   4493       lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata);
   4494     }
   4495   }
   4496 
   4497   rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response);
   4498   free(lbinfo_callback_data);
   4499 }
   4500 
   4501 static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client,
   4502     const rc_api_fetch_leaderboard_info_request_t* lbinfo_request,
   4503     rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata)
   4504 {
   4505   rc_client_fetch_leaderboard_entries_callback_data_t* callback_data;
   4506   rc_client_async_handle_t* async_handle;
   4507   rc_api_request_t request;
   4508   int result;
   4509   const char* error_message;
   4510 
   4511   result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request);
   4512 
   4513   if (result != RC_OK) {
   4514     error_message = rc_error_str(result);
   4515     callback(result, error_message, NULL, client, callback_userdata);
   4516     return NULL;
   4517   }
   4518 
   4519   callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data));
   4520   if (!callback_data) {
   4521     callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata);
   4522     return NULL;
   4523   }
   4524 
   4525   callback_data->client = client;
   4526   callback_data->callback = callback;
   4527   callback_data->callback_userdata = callback_userdata;
   4528   callback_data->leaderboard_id = lbinfo_request->leaderboard_id;
   4529 
   4530   async_handle = &callback_data->async_handle;
   4531   rc_client_begin_async(client, async_handle);
   4532   client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client);
   4533   rc_api_destroy_request(&request);
   4534 
   4535   return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL;
   4536 }
   4537 
   4538 rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id,
   4539     uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata)
   4540 {
   4541   rc_api_fetch_leaderboard_info_request_t lbinfo_request;
   4542 
   4543 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4544   if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries)
   4545     return client->state.external_client->begin_fetch_leaderboard_entries(client, leaderboard_id, first_entry, count, callback, callback_userdata);
   4546 #endif
   4547 
   4548   memset(&lbinfo_request, 0, sizeof(lbinfo_request));
   4549   lbinfo_request.leaderboard_id = leaderboard_id;
   4550   lbinfo_request.first_entry = first_entry;
   4551   lbinfo_request.count = count;
   4552 
   4553   return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata);
   4554 }
   4555 
   4556 rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id,
   4557   uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata)
   4558 {
   4559   rc_api_fetch_leaderboard_info_request_t lbinfo_request;
   4560 
   4561 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4562   if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries_around_user)
   4563     return client->state.external_client->begin_fetch_leaderboard_entries_around_user(client, leaderboard_id, count, callback, callback_userdata);
   4564 #endif
   4565 
   4566   memset(&lbinfo_request, 0, sizeof(lbinfo_request));
   4567   lbinfo_request.leaderboard_id = leaderboard_id;
   4568   lbinfo_request.username = client->user.username;
   4569   lbinfo_request.count = count;
   4570 
   4571   if (!lbinfo_request.username) {
   4572     callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata);
   4573     return NULL;
   4574   }
   4575 
   4576   return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata);
   4577 }
   4578 
   4579 void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list)
   4580 {
   4581   rc_client_leaderboard_entry_list_info_t* info = (rc_client_leaderboard_entry_list_info_t*)list;
   4582   if (info->destroy_func)
   4583     info->destroy_func(info);
   4584   else
   4585     free(list);
   4586 }
   4587 
   4588 int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size)
   4589 {
   4590   if (!entry)
   4591     return RC_INVALID_STATE;
   4592 
   4593   return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user);
   4594 }
   4595 
   4596 /* ===== Rich Presence ===== */
   4597 
   4598 static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data)
   4599 {
   4600   rc_client_t* client = (rc_client_t*)callback_data;
   4601   rc_api_ping_response_t response;
   4602 
   4603   int result = rc_api_process_ping_server_response(&response, server_response);
   4604   const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response);
   4605   if (error_message) {
   4606     RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message);
   4607   }
   4608 
   4609   rc_api_destroy_ping_response(&response);
   4610 }
   4611 
   4612 static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
   4613 {
   4614   rc_api_ping_request_t api_params;
   4615   rc_api_request_t request;
   4616   char buffer[256];
   4617   int result;
   4618 
   4619   if (!client->callbacks.rich_presence_override ||
   4620       !client->callbacks.rich_presence_override(client, buffer, sizeof(buffer))) {
   4621     rc_mutex_lock(&client->state.mutex);
   4622 
   4623     rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer),
   4624         client->state.legacy_peek, client, NULL);
   4625 
   4626     rc_mutex_unlock(&client->state.mutex);
   4627   }
   4628 
   4629   memset(&api_params, 0, sizeof(api_params));
   4630   api_params.username = client->user.username;
   4631   api_params.api_token = client->user.token;
   4632   api_params.game_id = client->game->public_.id;
   4633   api_params.rich_presence = buffer;
   4634   api_params.game_hash = client->game->public_.hash;
   4635   api_params.hardcore = client->state.hardcore;
   4636 
   4637   result = rc_api_init_ping_request(&request, &api_params);
   4638   if (result != RC_OK) {
   4639     RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result));
   4640   }
   4641   else {
   4642     client->callbacks.server_call(&request, rc_client_ping_callback, client, client);
   4643   }
   4644 
   4645   callback_data->when = now + 120 * 1000;
   4646   rc_client_schedule_callback(client, callback_data);
   4647 }
   4648 
   4649 int rc_client_has_rich_presence(rc_client_t* client)
   4650 {
   4651   if (!client)
   4652     return 0;
   4653 
   4654 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4655   if (client->state.external_client && client->state.external_client->has_rich_presence)
   4656     return client->state.external_client->has_rich_presence();
   4657 #endif
   4658 
   4659   if (!client->game || !client->game->runtime.richpresence || !client->game->runtime.richpresence->richpresence)
   4660     return 0;
   4661 
   4662   return 1;
   4663 }
   4664 
   4665 size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size)
   4666 {
   4667   int result;
   4668 
   4669   if (!client || !buffer)
   4670     return 0;
   4671 
   4672 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4673   if (client->state.external_client && client->state.external_client->get_rich_presence_message)
   4674     return client->state.external_client->get_rich_presence_message(buffer, buffer_size);
   4675 #endif
   4676 
   4677   if (!client->game)
   4678     return 0;
   4679 
   4680   rc_mutex_lock(&client->state.mutex);
   4681 
   4682   result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size,
   4683       client->state.legacy_peek, client, NULL);
   4684 
   4685   rc_mutex_unlock(&client->state.mutex);
   4686 
   4687   if (result == 0) {
   4688     result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title);
   4689     /* snprintf will return the amount of space needed, we want to return the number of chars written */
   4690     if ((size_t)result >= buffer_size)
   4691       return (buffer_size - 1);
   4692   }
   4693 
   4694   return result;
   4695 }
   4696 
   4697 int rc_client_get_rich_presence_strings(rc_client_t* client, const char** buffer, size_t buffer_size, size_t* count) {
   4698   int result;
   4699 
   4700   if (!client || !buffer)
   4701     return RC_INVALID_STATE;
   4702 
   4703   rc_mutex_lock(&client->state.mutex);
   4704   result = rc_runtime_get_richpresence_strings(&client->game->runtime, buffer, buffer_size, count);
   4705   rc_mutex_unlock(&client->state.mutex);
   4706   return result;
   4707 }
   4708 
   4709 /* ===== Processing ===== */
   4710 
   4711 void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler)
   4712 {
   4713   if (!client)
   4714     return;
   4715 
   4716 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4717   if (client->state.external_client && client->state.external_client->set_event_handler)
   4718     client->state.external_client->set_event_handler(client, handler);
   4719 #endif
   4720 
   4721   client->callbacks.event_handler = handler;
   4722 }
   4723 
   4724 void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler)
   4725 {
   4726   if (!client)
   4727     return;
   4728 
   4729 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4730   if (client->state.external_client && client->state.external_client->set_read_memory)
   4731     client->state.external_client->set_read_memory(client, handler);
   4732 #endif
   4733 
   4734   client->callbacks.read_memory = handler;
   4735 }
   4736 
   4737 static void rc_client_invalidate_processing_memref(rc_client_t* client)
   4738 {
   4739   rc_memref_t** next_memref = &client->game->runtime.memrefs;
   4740   rc_memref_t* memref;
   4741 
   4742   /* if processing_memref is not set, this occurred following a pointer chain. ignore it. */
   4743   if (!client->state.processing_memref)
   4744     return;
   4745 
   4746   /* invalid memref. remove from chain so we don't have to evaluate it in the future.
   4747    * it's still there, so anything referencing it will always fetch the current value. */
   4748   while ((memref = *next_memref) != NULL) {
   4749     if (memref == client->state.processing_memref) {
   4750       *next_memref = memref->next;
   4751       break;
   4752     }
   4753     next_memref = &memref->next;
   4754   }
   4755 
   4756   rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref);
   4757   rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref);
   4758 
   4759   client->state.processing_memref = NULL;
   4760 }
   4761 
   4762 static uint32_t rc_client_peek_le(uint32_t address, uint32_t num_bytes, void* ud)
   4763 {
   4764   rc_client_t* client = (rc_client_t*)ud;
   4765   uint32_t value = 0;
   4766   uint32_t num_read = 0;
   4767 
   4768   /* if we know the address is out of range, and it's part of a pointer chain
   4769    * (processing_memref is null), don't bother processing it. */
   4770   if (address > client->game->max_valid_address && !client->state.processing_memref)
   4771     return 0;
   4772 
   4773   if (num_bytes <= sizeof(value)) {
   4774     num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client);
   4775     if (num_read == num_bytes)
   4776       return value;
   4777   }
   4778 
   4779   if (num_read < num_bytes)
   4780     rc_client_invalidate_processing_memref(client);
   4781 
   4782   return 0;
   4783 }
   4784 
   4785 static uint32_t rc_client_peek(uint32_t address, uint32_t num_bytes, void* ud)
   4786 {
   4787   rc_client_t* client = (rc_client_t*)ud;
   4788   uint8_t buffer[4];
   4789   uint32_t num_read = 0;
   4790 
   4791   /* if we know the address is out of range, and it's part of a pointer chain
   4792    * (processing_memref is null), don't bother processing it. */
   4793   if (address > client->game->max_valid_address && !client->state.processing_memref)
   4794     return 0;
   4795 
   4796   switch (num_bytes) {
   4797     case 1:
   4798       num_read = client->callbacks.read_memory(address, buffer, 1, client);
   4799       if (num_read == 1)
   4800         return buffer[0];
   4801       break;
   4802     case 2:
   4803       num_read = client->callbacks.read_memory(address, buffer, 2, client);
   4804       if (num_read == 2)
   4805         return buffer[0] | (buffer[1] << 8);
   4806       break;
   4807     case 3:
   4808       num_read = client->callbacks.read_memory(address, buffer, 3, client);
   4809       if (num_read == 3)
   4810         return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16);
   4811       break;
   4812     case 4:
   4813       num_read = client->callbacks.read_memory(address, buffer, 4, client);
   4814       if (num_read == 4)
   4815         return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24);
   4816       break;
   4817     default:
   4818       break;
   4819   }
   4820 
   4821   if (num_read < num_bytes)
   4822     rc_client_invalidate_processing_memref(client);
   4823 
   4824   return 0;
   4825 }
   4826 
   4827 void rc_client_set_legacy_peek(rc_client_t* client, int method)
   4828 {
   4829   if (method == RC_CLIENT_LEGACY_PEEK_AUTO) {
   4830     union {
   4831       uint32_t whole;
   4832       uint8_t parts[4];
   4833     } u;
   4834     u.whole = 1;
   4835     method = (u.parts[0] == 1) ?
   4836         RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED;
   4837   }
   4838 
   4839   client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ?
   4840       rc_client_peek_le : rc_client_peek;
   4841 }
   4842 
   4843 int rc_client_is_processing_required(rc_client_t* client)
   4844 {
   4845   if (!client)
   4846     return 0;
   4847 
   4848 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   4849   if (client->state.external_client && client->state.external_client->is_processing_required)
   4850     return client->state.external_client->is_processing_required();
   4851 #endif
   4852 
   4853   if (!client->game)
   4854     return 0;
   4855 
   4856   if (client->game->runtime.trigger_count || client->game->runtime.lboard_count)
   4857     return 1;
   4858 
   4859   return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence);
   4860 }
   4861 
   4862 static void rc_client_update_memref_values(rc_client_t* client)
   4863 {
   4864   rc_memref_t* memref = client->game->runtime.memrefs;
   4865   uint32_t value;
   4866   int invalidated_memref = 0;
   4867 
   4868   for (; memref; memref = memref->next) {
   4869     if (memref->value.is_indirect)
   4870       continue;
   4871 
   4872     client->state.processing_memref = memref;
   4873 
   4874     value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client);
   4875 
   4876     if (client->state.processing_memref) {
   4877       rc_update_memref_value(&memref->value, value);
   4878     }
   4879     else {
   4880       /* if the peek function cleared the processing_memref, the memref was invalidated */
   4881       invalidated_memref = 1;
   4882     }
   4883   }
   4884 
   4885   client->state.processing_memref = NULL;
   4886 
   4887   if (invalidated_memref)
   4888     rc_client_update_active_achievements(client->game);
   4889 }
   4890 
   4891 static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset)
   4892 {
   4893   rc_client_achievement_info_t* achievement = subset->achievements;
   4894   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   4895 
   4896   for (; achievement < stop; ++achievement) {
   4897     rc_trigger_t* trigger = achievement->trigger;
   4898     int old_state, new_state;
   4899     uint32_t old_measured_value;
   4900 
   4901     if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
   4902       continue;
   4903 
   4904     old_measured_value = trigger->measured_value;
   4905     old_state = trigger->state;
   4906     new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL);
   4907 
   4908     /* trigger->state doesn't actually change to RESET - RESET just serves as a notification.
   4909      * we don't care about that particular notification, so look at the actual state. */
   4910     if (new_state == RC_TRIGGER_STATE_RESET)
   4911       new_state = trigger->state;
   4912 
   4913     /* if the measured value changed and the achievement hasn't triggered, show a progress indicator */
   4914     if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN &&
   4915         trigger->measured_value <= trigger->measured_target &&
   4916         rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) {
   4917 
   4918       /* only show a popup for the achievement closest to triggering */
   4919       float progress = (float)trigger->measured_value / (float)trigger->measured_target;
   4920 
   4921       if (trigger->measured_as_percent) {
   4922         /* if reporting the measured value as a percentage, only show the popup if the percentage changes */
   4923         const uint32_t old_percent = (uint32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target);
   4924         const uint32_t new_percent = (uint32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target);
   4925         if (old_percent == new_percent)
   4926           progress = -1.0;
   4927       }
   4928 
   4929       if (progress > client->game->progress_tracker.progress) {
   4930         client->game->progress_tracker.progress = progress;
   4931         client->game->progress_tracker.achievement = achievement;
   4932         client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER;
   4933         subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
   4934         achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE;
   4935       }
   4936     }
   4937 
   4938     /* if the state hasn't changed, there won't be any events raised */
   4939     if (new_state == old_state)
   4940       continue;
   4941 
   4942     /* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */
   4943     if (old_state == RC_TRIGGER_STATE_PRIMED)
   4944       achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
   4945 
   4946     /* raise events for each of the possible new states */
   4947     if (new_state == RC_TRIGGER_STATE_TRIGGERED)
   4948       achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED;
   4949     else if (new_state == RC_TRIGGER_STATE_PRIMED)
   4950       achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW;
   4951 
   4952     subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
   4953   }
   4954 }
   4955 
   4956 static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game)
   4957 {
   4958   /* ASSERT: this should only be called if the mutex is held */
   4959 
   4960   if (game->progress_tracker.hide_callback &&
   4961       game->progress_tracker.hide_callback->when &&
   4962       game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) {
   4963     rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0);
   4964     game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE;
   4965     game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER;
   4966   }
   4967 }
   4968 
   4969 static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
   4970 {
   4971   rc_client_event_t client_event;
   4972   memset(&client_event, 0, sizeof(client_event));
   4973 
   4974   (void)callback_data;
   4975   (void)now;
   4976 
   4977   rc_mutex_lock(&client->state.mutex);
   4978   if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) {
   4979     client->game->progress_tracker.hide_callback->when = 0;
   4980     client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE;
   4981   }
   4982   rc_mutex_unlock(&client->state.mutex);
   4983 
   4984   if (client_event.type)
   4985     client->callbacks.event_handler(&client_event, client);
   4986 }
   4987 
   4988 static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game)
   4989 {
   4990   /* ASSERT: this should only be called if the mutex is held */
   4991 
   4992   if (!game->progress_tracker.hide_callback) {
   4993     game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*)
   4994       rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t));
   4995     memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t));
   4996     game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed;
   4997   }
   4998 
   4999   if (game->progress_tracker.hide_callback->when == 0)
   5000     game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW;
   5001   else
   5002     game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE;
   5003 
   5004   rc_client_reschedule_callback(client, game->progress_tracker.hide_callback,
   5005       client->callbacks.get_time_millisecs(client) + 2 * 1000);
   5006 }
   5007 
   5008 static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game)
   5009 {
   5010   rc_client_event_t client_event;
   5011 
   5012   memset(&client_event, 0, sizeof(client_event));
   5013 
   5014   switch (game->progress_tracker.action) {
   5015   case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW:
   5016     client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW;
   5017     break;
   5018   case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE:
   5019     client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE;
   5020     break;
   5021   default:
   5022     client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE;
   5023     break;
   5024   }
   5025   game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE;
   5026 
   5027   client_event.achievement = &game->progress_tracker.achievement->public_;
   5028   client->callbacks.event_handler(&client_event, client);
   5029 }
   5030 
   5031 static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset)
   5032 {
   5033   rc_client_achievement_info_t* achievement = subset->achievements;
   5034   rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
   5035   rc_client_event_t client_event;
   5036   time_t recent_unlock_time = 0;
   5037 
   5038   memset(&client_event, 0, sizeof(client_event));
   5039 
   5040   for (; achievement < stop; ++achievement) {
   5041     if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE)
   5042       continue;
   5043 
   5044     /* kick off award achievement request first */
   5045     if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) {
   5046       rc_client_award_achievement(client, achievement);
   5047       client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS;
   5048     }
   5049 
   5050     /* update display state */
   5051     if (recent_unlock_time == 0)
   5052       recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS;
   5053     rc_client_update_achievement_display_information(client, achievement, recent_unlock_time);
   5054 
   5055     /* raise events */
   5056     client_event.achievement = &achievement->public_;
   5057 
   5058     if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) {
   5059       client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE;
   5060       client->callbacks.event_handler(&client_event, client);
   5061     }
   5062     else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) {
   5063       client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW;
   5064       client->callbacks.event_handler(&client_event, client);
   5065     }
   5066 
   5067     if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) {
   5068       client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED;
   5069       client->callbacks.event_handler(&client_event, client);
   5070     }
   5071 
   5072     /* clear pending flags */
   5073     achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE;
   5074   }
   5075 }
   5076 
   5077 static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset)
   5078 {
   5079   rc_client_event_t client_event;
   5080 
   5081   memset(&client_event, 0, sizeof(client_event));
   5082   client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED;
   5083 
   5084   subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN;
   5085 
   5086   client->callbacks.event_handler(&client_event, client);
   5087 }
   5088 
   5089 static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset)
   5090 {
   5091   rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
   5092   rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
   5093 
   5094   for (; leaderboard < stop; ++leaderboard) {
   5095     rc_lboard_t* lboard = leaderboard->lboard;
   5096     int old_state, new_state;
   5097 
   5098     switch (leaderboard->public_.state) {
   5099       case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
   5100       case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
   5101         continue;
   5102 
   5103       default:
   5104         if (!lboard)
   5105           continue;
   5106 
   5107         break;
   5108     }
   5109 
   5110     old_state = lboard->state;
   5111     new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL);
   5112 
   5113     switch (new_state) {
   5114       case RC_LBOARD_STATE_STARTED: /* leaderboard is running */
   5115         if (old_state != RC_LBOARD_STATE_STARTED) {
   5116           leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING;
   5117           leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED;
   5118           rc_client_allocate_leaderboard_tracker(client->game, leaderboard);
   5119         }
   5120         else {
   5121           rc_client_update_leaderboard_tracker(client->game, leaderboard);
   5122         }
   5123         break;
   5124 
   5125       case RC_LBOARD_STATE_CANCELED:
   5126         if (old_state != RC_LBOARD_STATE_CANCELED) {
   5127           leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
   5128           leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
   5129           rc_client_release_leaderboard_tracker(client->game, leaderboard);
   5130         }
   5131         break;
   5132 
   5133       case RC_LBOARD_STATE_TRIGGERED:
   5134         if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) {
   5135           leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
   5136           leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED;
   5137 
   5138           if (old_state != RC_LBOARD_STATE_STARTED)
   5139             rc_client_allocate_leaderboard_tracker(client->game, leaderboard);
   5140           else
   5141             rc_client_update_leaderboard_tracker(client->game, leaderboard);
   5142 
   5143           rc_client_release_leaderboard_tracker(client->game, leaderboard);
   5144         }
   5145         break;
   5146     }
   5147 
   5148     if (leaderboard->pending_events)
   5149       subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD;
   5150   }
   5151 }
   5152 
   5153 static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game)
   5154 {
   5155   rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers;
   5156   rc_client_event_t client_event;
   5157 
   5158   memset(&client_event, 0, sizeof(client_event));
   5159 
   5160   tracker = game->leaderboard_trackers;
   5161   for (; tracker; tracker = tracker->next) {
   5162     if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE)
   5163       continue;
   5164 
   5165     client_event.leaderboard_tracker = &tracker->public_;
   5166 
   5167     /* update display text for new trackers or updated trackers */
   5168     if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE))
   5169       rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format);
   5170 
   5171     if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) {
   5172       if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) {
   5173         /* request to show and hide in the same frame - ignore the event */
   5174       }
   5175       else {
   5176         client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE;
   5177         client->callbacks.event_handler(&client_event, client);
   5178       }
   5179     }
   5180     else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) {
   5181       client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW;
   5182       client->callbacks.event_handler(&client_event, client);
   5183     }
   5184     else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) {
   5185       client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE;
   5186       client->callbacks.event_handler(&client_event, client);
   5187     }
   5188 
   5189     tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE;
   5190   }
   5191 }
   5192 
   5193 static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset)
   5194 {
   5195   rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
   5196   rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
   5197   rc_client_event_t client_event;
   5198 
   5199   memset(&client_event, 0, sizeof(client_event));
   5200 
   5201   for (; leaderboard < leaderboard_stop; ++leaderboard) {
   5202     if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE)
   5203       continue;
   5204 
   5205     client_event.leaderboard = &leaderboard->public_;
   5206 
   5207     if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) {
   5208       RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title);
   5209       client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED;
   5210       client->callbacks.event_handler(&client_event, client);
   5211     }
   5212     else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) {
   5213       /* kick off submission request before raising event */
   5214       rc_client_submit_leaderboard_entry(client, leaderboard);
   5215 
   5216       client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED;
   5217       client->callbacks.event_handler(&client_event, client);
   5218     }
   5219     else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) {
   5220       RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title);
   5221       client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED;
   5222       client->callbacks.event_handler(&client_event, client);
   5223     }
   5224 
   5225     leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE;
   5226   }
   5227 }
   5228 
   5229 static void rc_client_reset_pending_events(rc_client_t* client)
   5230 {
   5231   rc_client_subset_info_t* subset;
   5232 
   5233   client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE;
   5234 
   5235   for (subset = client->game->subsets; subset; subset = subset->next)
   5236     subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE;
   5237 }
   5238 
   5239 static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset)
   5240 {
   5241   /* raise any pending achievement events */
   5242   if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT)
   5243     rc_client_raise_achievement_events(client, subset);
   5244 
   5245   /* raise any pending leaderboard events */
   5246   if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD)
   5247     rc_client_raise_leaderboard_events(client, subset);
   5248 
   5249   /* raise mastery event if pending */
   5250   if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING)
   5251     rc_client_raise_mastery_event(client, subset);
   5252 }
   5253 
   5254 static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game)
   5255 {
   5256   rc_client_subset_info_t* subset;
   5257 
   5258   /* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */
   5259   if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER)
   5260     rc_client_raise_leaderboard_tracker_events(client, game);
   5261 
   5262   for (subset = game->subsets; subset; subset = subset->next)
   5263     rc_client_subset_raise_pending_events(client, subset);
   5264 
   5265   /* raise progress tracker events after achievement events so formatted values are updated for tracker event */
   5266   if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER)
   5267     rc_client_raise_progress_tracker_events(client, game);
   5268 
   5269   /* if any achievements were unlocked, resync the active achievements list */
   5270   if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) {
   5271     rc_mutex_lock(&client->state.mutex);
   5272     rc_client_update_active_achievements(game);
   5273     rc_mutex_unlock(&client->state.mutex);
   5274   }
   5275 
   5276   game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE;
   5277 }
   5278 
   5279 void rc_client_do_frame(rc_client_t* client)
   5280 {
   5281   if (!client)
   5282     return;
   5283 
   5284 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5285   if (client->state.external_client && client->state.external_client->do_frame) {
   5286     client->state.external_client->do_frame();
   5287     return;
   5288   }
   5289 #endif
   5290 
   5291   if (client->game && !client->game->waiting_for_reset) {
   5292     rc_runtime_richpresence_t* richpresence;
   5293     rc_client_subset_info_t* subset;
   5294 
   5295     rc_mutex_lock(&client->state.mutex);
   5296 
   5297     rc_client_reset_pending_events(client);
   5298 
   5299     rc_client_update_memref_values(client);
   5300     rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL);
   5301 
   5302     client->game->progress_tracker.progress = 0.0;
   5303     for (subset = client->game->subsets; subset; subset = subset->next) {
   5304       if (subset->active)
   5305         rc_client_do_frame_process_achievements(client, subset);
   5306     }
   5307     if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER)
   5308       rc_client_do_frame_update_progress_tracker(client, client->game);
   5309 
   5310     if (client->state.hardcore || client->state.allow_leaderboards_in_softcore) {
   5311       for (subset = client->game->subsets; subset; subset = subset->next) {
   5312         if (subset->active)
   5313           rc_client_do_frame_process_leaderboards(client, subset);
   5314       }
   5315     }
   5316 
   5317     richpresence = client->game->runtime.richpresence;
   5318     if (richpresence && richpresence->richpresence)
   5319       rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL);
   5320 
   5321     rc_mutex_unlock(&client->state.mutex);
   5322 
   5323     rc_client_raise_pending_events(client, client->game);
   5324   }
   5325 
   5326   /* we've processed a frame. if there's a pause delay in effect, process it */
   5327   if (client->state.unpaused_frame_decay > 0) {
   5328     client->state.unpaused_frame_decay--;
   5329 
   5330     if (client->state.unpaused_frame_decay == 0 &&
   5331         client->state.required_unpaused_frames > RC_MINIMUM_UNPAUSED_FRAMES) {
   5332       /* the full decay has elapsed and a penalty still exists.
   5333        * lower the penalty and reset the decay counter */
   5334       client->state.required_unpaused_frames >>= 1;
   5335 
   5336       if (client->state.required_unpaused_frames <= RC_MINIMUM_UNPAUSED_FRAMES)
   5337         client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES;
   5338 
   5339       client->state.unpaused_frame_decay =
   5340         client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1) - 1;
   5341     }
   5342   }
   5343 
   5344   rc_client_idle(client);
   5345 }
   5346 
   5347 void rc_client_idle(rc_client_t* client)
   5348 {
   5349   rc_client_scheduled_callback_data_t* scheduled_callback;
   5350 
   5351   if (!client)
   5352     return;
   5353 
   5354 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5355   if (client->state.external_client && client->state.external_client->idle) {
   5356     client->state.external_client->idle();
   5357     return;
   5358   }
   5359 #endif
   5360 
   5361   scheduled_callback = client->state.scheduled_callbacks;
   5362   if (scheduled_callback) {
   5363     const rc_clock_t now = client->callbacks.get_time_millisecs(client);
   5364 
   5365     do {
   5366       rc_mutex_lock(&client->state.mutex);
   5367       scheduled_callback = client->state.scheduled_callbacks;
   5368       if (scheduled_callback) {
   5369         if (scheduled_callback->when > now) {
   5370           /* not time for next callback yet, ignore it */
   5371           scheduled_callback = NULL;
   5372         }
   5373         else {
   5374           /* remove the callback from the queue while we process it. callback can requeue if desired */
   5375           client->state.scheduled_callbacks = scheduled_callback->next;
   5376           scheduled_callback->next = NULL;
   5377         }
   5378       }
   5379       rc_mutex_unlock(&client->state.mutex);
   5380 
   5381       if (!scheduled_callback)
   5382         break;
   5383 
   5384       scheduled_callback->callback(scheduled_callback, client, now);
   5385     } while (1);
   5386   }
   5387 
   5388   if (client->state.disconnect & ~RC_CLIENT_DISCONNECT_VISIBLE)
   5389     rc_client_raise_disconnect_events(client);
   5390 }
   5391 
   5392 void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback)
   5393 {
   5394   rc_client_scheduled_callback_data_t** last;
   5395   rc_client_scheduled_callback_data_t* next;
   5396 
   5397   rc_mutex_lock(&client->state.mutex);
   5398 
   5399   last = &client->state.scheduled_callbacks;
   5400   do {
   5401     next = *last;
   5402     if (!next || scheduled_callback->when < next->when) {
   5403       scheduled_callback->next = next;
   5404       *last = scheduled_callback;
   5405       break;
   5406     }
   5407 
   5408     last = &next->next;
   5409   } while (1);
   5410 
   5411   rc_mutex_unlock(&client->state.mutex);
   5412 }
   5413 
   5414 static void rc_client_reschedule_callback(rc_client_t* client,
   5415   rc_client_scheduled_callback_data_t* callback, rc_clock_t when)
   5416 {
   5417   rc_client_scheduled_callback_data_t** last;
   5418   rc_client_scheduled_callback_data_t* next;
   5419 
   5420   /* ASSERT: this should only be called if the mutex is held */
   5421 
   5422   callback->when = when;
   5423 
   5424   last = &client->state.scheduled_callbacks;
   5425   do {
   5426     next = *last;
   5427 
   5428     if (next == callback) {
   5429       if (when == 0) {
   5430         /* request to unschedule the callback */
   5431         *last = next->next;
   5432         next->next = NULL;
   5433         break;
   5434       }
   5435 
   5436       if (!next->next) {
   5437          /* end of list, just append it */
   5438          break;
   5439       }
   5440 
   5441       if (when < next->next->when) {
   5442         /* already in the correct place */
   5443         break;
   5444       }
   5445 
   5446       /* remove from current position - will insert later */
   5447       *last = next->next;
   5448       next->next = NULL;
   5449       continue;
   5450     }
   5451 
   5452     if (!next || (when < next->when && when != 0)) {
   5453       /* insert here */
   5454       callback->next = next;
   5455       *last = callback;
   5456       break;
   5457     }
   5458 
   5459     last = &next->next;
   5460   } while (1);
   5461 }
   5462 
   5463 static void rc_client_reset_richpresence(rc_client_t* client)
   5464 {
   5465   rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence;
   5466   if (richpresence && richpresence->richpresence)
   5467     rc_reset_richpresence(richpresence->richpresence);
   5468 }
   5469 
   5470 static void rc_client_reset_variables(rc_client_t* client)
   5471 {
   5472   rc_value_t* variable = client->game->runtime.variables;
   5473   for (; variable; variable = variable->next)
   5474     rc_reset_value(variable);
   5475 }
   5476 
   5477 static void rc_client_reset_all(rc_client_t* client)
   5478 {
   5479   rc_client_reset_achievements(client);
   5480   rc_client_reset_leaderboards(client);
   5481   rc_client_reset_richpresence(client);
   5482   rc_client_reset_variables(client);
   5483 }
   5484 
   5485 void rc_client_reset(rc_client_t* client)
   5486 {
   5487   rc_client_game_hash_t* game_hash;
   5488   if (!client)
   5489     return;
   5490 
   5491 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5492   if (client->state.external_client && client->state.external_client->reset) {
   5493     client->state.external_client->reset();
   5494     return;
   5495   }
   5496 #endif
   5497 
   5498   if (!client->game)
   5499     return;
   5500 
   5501   game_hash = rc_client_find_game_hash(client, client->game->public_.hash);
   5502   if (game_hash && game_hash->game_id != client->game->public_.id) {
   5503     /* current media is not for loaded game. unload game */
   5504     RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)",
   5505         (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash);
   5506     rc_client_unload_game(client);
   5507     return;
   5508   }
   5509 
   5510   RC_CLIENT_LOG_INFO(client, "Resetting runtime");
   5511 
   5512   rc_mutex_lock(&client->state.mutex);
   5513 
   5514   client->game->waiting_for_reset = 0;
   5515   rc_client_reset_pending_events(client);
   5516 
   5517   rc_client_hide_progress_tracker(client, client->game);
   5518   rc_client_reset_all(client);
   5519 
   5520   rc_mutex_unlock(&client->state.mutex);
   5521 
   5522   rc_client_raise_pending_events(client, client->game);
   5523 }
   5524 
   5525 int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining)
   5526 {
   5527 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5528   if (client->state.external_client && client->state.external_client->can_pause)
   5529     return client->state.external_client->can_pause(frames_remaining);
   5530 #endif
   5531 
   5532   if (frames_remaining)
   5533     *frames_remaining = 0;
   5534 
   5535   /* pause is always allowed in softcore */
   5536   if (!rc_client_get_hardcore_enabled(client))
   5537     return 1;
   5538 
   5539   /* a full decay means we haven't processed any frames since the last time this was called. */
   5540   if (client->state.unpaused_frame_decay == client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER)
   5541     return 1;
   5542 
   5543   /* if less than RC_MINIMUM_UNPAUSED_FRAMES have been processed, don't allow the pause */
   5544   if (client->state.unpaused_frame_decay > client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1)) {
   5545     if (frames_remaining) {
   5546       *frames_remaining = client->state.unpaused_frame_decay -
   5547                           client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1);
   5548     }
   5549     return 0;
   5550   }
   5551 
   5552   /* we're going to allow the emulator to pause. calculate how many frames are needed before the next
   5553    * pause will be allowed. */
   5554 
   5555   if (client->state.unpaused_frame_decay > 0) {
   5556     /* The user has paused within the decay window. Require a longer
   5557      * run of unpaused frames before allowing the next pause */
   5558     if (client->state.required_unpaused_frames < 5 * 60) /* don't make delay longer then 5 seconds */
   5559       client->state.required_unpaused_frames += RC_MINIMUM_UNPAUSED_FRAMES;
   5560   }
   5561 
   5562   /* require multiple unpaused_frames windows to decay the penalty */
   5563   client->state.unpaused_frame_decay = client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER;
   5564 
   5565   return 1;
   5566 }
   5567 
   5568 size_t rc_client_progress_size(rc_client_t* client)
   5569 {
   5570   size_t result;
   5571 
   5572   if (!client)
   5573     return 0;
   5574 
   5575 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5576   if (client->state.external_client && client->state.external_client->progress_size)
   5577     return client->state.external_client->progress_size();
   5578 #endif
   5579 
   5580   if (!rc_client_is_game_loaded(client))
   5581     return 0;
   5582 
   5583   rc_mutex_lock(&client->state.mutex);
   5584   result = rc_runtime_progress_size(&client->game->runtime, NULL);
   5585   rc_mutex_unlock(&client->state.mutex);
   5586 
   5587   return result;
   5588 }
   5589 
   5590 int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer)
   5591 {
   5592   return rc_client_serialize_progress_sized(client, buffer, 0xFFFFFFFF);
   5593 }
   5594 
   5595 int rc_client_serialize_progress_sized(rc_client_t* client, uint8_t* buffer, size_t buffer_size)
   5596 {
   5597   int result;
   5598 
   5599   if (!client)
   5600     return RC_NO_GAME_LOADED;
   5601 
   5602 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5603   if (client->state.external_client && client->state.external_client->serialize_progress)
   5604     return client->state.external_client->serialize_progress(buffer, buffer_size);
   5605 #endif
   5606 
   5607   if (!rc_client_is_game_loaded(client))
   5608     return RC_NO_GAME_LOADED;
   5609 
   5610   if (!buffer)
   5611     return RC_INVALID_STATE;
   5612 
   5613   rc_mutex_lock(&client->state.mutex);
   5614   result = rc_runtime_serialize_progress_sized(buffer, (uint32_t)buffer_size, &client->game->runtime, NULL);
   5615   rc_mutex_unlock(&client->state.mutex);
   5616 
   5617   return result;
   5618 }
   5619 
   5620 static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset)
   5621 {
   5622   rc_client_achievement_info_t* achievement;
   5623   rc_client_achievement_info_t* achievement_stop;
   5624   rc_client_leaderboard_info_t* leaderboard;
   5625   rc_client_leaderboard_info_t* leaderboard_stop;
   5626 
   5627   /* flag any visible challenge indicators to be hidden */
   5628   achievement = subset->achievements;
   5629   achievement_stop = achievement + subset->public_.num_achievements;
   5630   for (; achievement < achievement_stop; ++achievement) {
   5631     rc_trigger_t* trigger = achievement->trigger;
   5632     if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED &&
   5633         achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) {
   5634       achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
   5635       subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
   5636     }
   5637   }
   5638 
   5639   /* flag any visible trackers to be hidden */
   5640   leaderboard = subset->leaderboards;
   5641   leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
   5642   for (; leaderboard < leaderboard_stop; ++leaderboard) {
   5643     rc_lboard_t* lboard = leaderboard->lboard;
   5644     if (lboard && lboard->state == RC_LBOARD_STATE_STARTED &&
   5645         leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) {
   5646       leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
   5647       subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD;
   5648     }
   5649   }
   5650 }
   5651 
   5652 static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset)
   5653 {
   5654   rc_client_achievement_info_t* achievement;
   5655   rc_client_achievement_info_t* achievement_stop;
   5656   rc_client_leaderboard_info_t* leaderboard;
   5657   rc_client_leaderboard_info_t* leaderboard_stop;
   5658 
   5659   /* flag any challenge indicators that should be shown */
   5660   achievement = subset->achievements;
   5661   achievement_stop = achievement + subset->public_.num_achievements;
   5662   for (; achievement < achievement_stop; ++achievement) {
   5663     rc_trigger_t* trigger = achievement->trigger;
   5664     if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
   5665       continue;
   5666 
   5667     if (trigger->state == RC_TRIGGER_STATE_PRIMED) {
   5668       /* if it's already shown, just keep it. otherwise flag it to be shown */
   5669       if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) {
   5670         achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
   5671       }
   5672       else {
   5673         achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW;
   5674         subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
   5675       }
   5676     }
   5677     /* ASSERT: only active achievements are serialized, so we don't have to worry about
   5678      *         deserialization deactiving them. */
   5679   }
   5680 
   5681   /* flag any trackers that need to be shown */
   5682   leaderboard = subset->leaderboards;
   5683   leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
   5684   for (; leaderboard < leaderboard_stop; ++leaderboard) {
   5685     rc_lboard_t* lboard = leaderboard->lboard;
   5686     if (!lboard ||
   5687         leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE ||
   5688         leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED)
   5689       continue;
   5690 
   5691     if (lboard->state == RC_LBOARD_STATE_STARTED) {
   5692       leaderboard->value = (int)lboard->value.value.value;
   5693       leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING;
   5694 
   5695       /* if it's already being tracked, just update tracker. otherwise, allocate one */
   5696       if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) {
   5697         leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
   5698         rc_client_update_leaderboard_tracker(game, leaderboard);
   5699       }
   5700       else {
   5701         rc_client_allocate_leaderboard_tracker(game, leaderboard);
   5702       }
   5703     }
   5704     else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) {
   5705       /* deallocate the tracker (don't actually raise the failed event) */
   5706       leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
   5707       leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
   5708       rc_client_release_leaderboard_tracker(game, leaderboard);
   5709     }
   5710   }
   5711 }
   5712 
   5713 int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized)
   5714 {
   5715   return rc_client_deserialize_progress_sized(client, serialized, 0xFFFFFFFF);
   5716 }
   5717 
   5718 int rc_client_deserialize_progress_sized(rc_client_t* client, const uint8_t* serialized, size_t serialized_size)
   5719 {
   5720   rc_client_subset_info_t* subset;
   5721   int result;
   5722 
   5723   if (!client)
   5724     return RC_NO_GAME_LOADED;
   5725 
   5726 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5727   if (client->state.external_client && client->state.external_client->deserialize_progress)
   5728     return client->state.external_client->deserialize_progress(serialized, serialized_size);
   5729 #endif
   5730 
   5731   if (!rc_client_is_game_loaded(client))
   5732     return RC_NO_GAME_LOADED;
   5733 
   5734   rc_mutex_lock(&client->state.mutex);
   5735 
   5736   rc_client_reset_pending_events(client);
   5737 
   5738   for (subset = client->game->subsets; subset; subset = subset->next)
   5739     rc_client_subset_before_deserialize_progress(subset);
   5740 
   5741   rc_client_hide_progress_tracker(client, client->game);
   5742 
   5743   if (!serialized) {
   5744     rc_client_reset_all(client);
   5745     result = RC_OK;
   5746   }
   5747   else {
   5748     result = rc_runtime_deserialize_progress_sized(&client->game->runtime, serialized, (uint32_t)serialized_size, NULL);
   5749   }
   5750 
   5751   for (subset = client->game->subsets; subset; subset = subset->next)
   5752     rc_client_subset_after_deserialize_progress(client->game, subset);
   5753 
   5754   rc_mutex_unlock(&client->state.mutex);
   5755 
   5756   rc_client_raise_pending_events(client, client->game);
   5757 
   5758   return result;
   5759 }
   5760 
   5761 /* ===== Toggles ===== */
   5762 
   5763 static void rc_client_enable_hardcore(rc_client_t* client)
   5764 {
   5765   client->state.hardcore = 1;
   5766 
   5767   if (client->game) {
   5768     rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE);
   5769     rc_client_activate_leaderboards(client->game, client);
   5770 
   5771     /* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */
   5772     RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset");
   5773     client->game->waiting_for_reset = 1;
   5774   }
   5775   else {
   5776     RC_CLIENT_LOG_INFO(client, "Hardcore enabled");
   5777   }
   5778 }
   5779 
   5780 static void rc_client_disable_hardcore(rc_client_t* client)
   5781 {
   5782   client->state.hardcore = 0;
   5783   RC_CLIENT_LOG_INFO(client, "Hardcore disabled");
   5784 
   5785   if (client->game) {
   5786     rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
   5787 
   5788     if (!client->state.allow_leaderboards_in_softcore)
   5789       rc_client_deactivate_leaderboards(client->game, client);
   5790   }
   5791 }
   5792 
   5793 void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled)
   5794 {
   5795   int changed = 0;
   5796 
   5797   if (!client)
   5798     return;
   5799 
   5800 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5801   if (client->state.external_client && client->state.external_client->get_hardcore_enabled) {
   5802     client->state.external_client->set_hardcore_enabled(enabled);
   5803     return;
   5804   }
   5805 #endif
   5806 
   5807   rc_mutex_lock(&client->state.mutex);
   5808 
   5809   enabled = enabled ? 1 : 0;
   5810   if (client->state.hardcore != enabled) {
   5811     if (enabled)
   5812       rc_client_enable_hardcore(client);
   5813     else
   5814       rc_client_disable_hardcore(client);
   5815 
   5816     changed = 1;
   5817   }
   5818 
   5819   rc_mutex_unlock(&client->state.mutex);
   5820 
   5821   /* events must be raised outside of lock */
   5822   if (changed && client->game) {
   5823     if (enabled) {
   5824       /* if enabling hardcore, notify client that a reset is requested */
   5825       if (client->game->waiting_for_reset) {
   5826         rc_client_event_t client_event;
   5827         memset(&client_event, 0, sizeof(client_event));
   5828         client_event.type = RC_CLIENT_EVENT_RESET;
   5829         client->callbacks.event_handler(&client_event, client);
   5830       }
   5831     }
   5832     else {
   5833       /* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */
   5834       rc_client_raise_pending_events(client, client->game);
   5835     }
   5836   }
   5837 }
   5838 
   5839 int rc_client_get_hardcore_enabled(const rc_client_t* client)
   5840 {
   5841   if (!client)
   5842     return 0;
   5843 
   5844 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5845   if (client->state.external_client && client->state.external_client->get_hardcore_enabled)
   5846     return client->state.external_client->get_hardcore_enabled();
   5847 #endif
   5848 
   5849   return client->state.hardcore;
   5850 }
   5851 
   5852 void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled)
   5853 {
   5854   if (!client)
   5855     return;
   5856 
   5857 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5858   if (client->state.external_client && client->state.external_client->set_unofficial_enabled) {
   5859     client->state.external_client->set_unofficial_enabled(enabled);
   5860     return;
   5861   }
   5862 #endif
   5863 
   5864   RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled");
   5865   client->state.unofficial_enabled = enabled ? 1 : 0;
   5866 }
   5867 
   5868 int rc_client_get_unofficial_enabled(const rc_client_t* client)
   5869 {
   5870   if (!client)
   5871     return 0;
   5872 
   5873 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5874   if (client->state.external_client && client->state.external_client->get_unofficial_enabled)
   5875     return client->state.external_client->get_unofficial_enabled();
   5876 #endif
   5877 
   5878   return client->state.unofficial_enabled;
   5879 }
   5880 
   5881 void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled)
   5882 {
   5883   if (!client)
   5884     return;
   5885 
   5886 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5887   if (client->state.external_client && client->state.external_client->set_encore_mode_enabled) {
   5888     client->state.external_client->set_encore_mode_enabled(enabled);
   5889     return;
   5890   }
   5891 #endif
   5892 
   5893   RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled");
   5894   client->state.encore_mode = enabled ? 1 : 0;
   5895 }
   5896 
   5897 int rc_client_get_encore_mode_enabled(const rc_client_t* client)
   5898 {
   5899   if (!client)
   5900     return 0;
   5901 
   5902 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5903   if (client->state.external_client && client->state.external_client->get_encore_mode_enabled)
   5904     return client->state.external_client->get_encore_mode_enabled();
   5905 #endif
   5906 
   5907   return client->state.encore_mode;
   5908 }
   5909 
   5910 void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled)
   5911 {
   5912   if (!client)
   5913     return;
   5914 
   5915 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5916   if (client->state.external_client && client->state.external_client->set_spectator_mode_enabled) {
   5917     client->state.external_client->set_spectator_mode_enabled(enabled);
   5918     return;
   5919   }
   5920 #endif
   5921 
   5922   if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) {
   5923     RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game.");
   5924     return;
   5925   }
   5926 
   5927   RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled");
   5928   client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF;
   5929 }
   5930 
   5931 int rc_client_get_spectator_mode_enabled(const rc_client_t* client)
   5932 {
   5933   if (!client)
   5934     return 0;
   5935 
   5936 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5937   if (client->state.external_client && client->state.external_client->get_spectator_mode_enabled)
   5938     return client->state.external_client->get_spectator_mode_enabled();
   5939 #endif
   5940 
   5941   return (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1;
   5942 }
   5943 
   5944 void rc_client_set_userdata(rc_client_t* client, void* userdata)
   5945 {
   5946   if (client)
   5947     client->callbacks.client_data = userdata;
   5948 }
   5949 
   5950 void* rc_client_get_userdata(const rc_client_t* client)
   5951 {
   5952   return client ? client->callbacks.client_data : NULL;
   5953 }
   5954 
   5955 void rc_client_set_host(const rc_client_t* client, const char* hostname)
   5956 {
   5957   /* if empty, just pass NULL */
   5958   if (hostname && !hostname[0])
   5959     hostname = NULL;
   5960 
   5961   /* clear the image host so it'll use the custom host for images too */
   5962   rc_api_set_image_host(NULL);
   5963 
   5964   /* set the custom host */
   5965   if (hostname && client) {
   5966     RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname);
   5967   }
   5968   rc_api_set_host(hostname);
   5969 
   5970 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5971   if (client && client->state.external_client && client->state.external_client->set_host)
   5972     client->state.external_client->set_host(hostname);
   5973 #endif
   5974 }
   5975 
   5976 size_t rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size)
   5977 {
   5978   size_t result;
   5979 
   5980 #ifdef RC_CLIENT_SUPPORTS_EXTERNAL
   5981   if (client && client->state.external_client && client->state.external_client->get_user_agent_clause) {
   5982     result = client->state.external_client->get_user_agent_clause(buffer, buffer_size);
   5983     if (result > 0) {
   5984       result += snprintf(buffer + result, buffer_size - result, " rc_client/" RCHEEVOS_VERSION_STRING);
   5985       buffer[buffer_size - 1] = '\0';
   5986       return result;
   5987     }
   5988   }
   5989 #else
   5990   (void)client;
   5991 #endif
   5992 
   5993   result = snprintf(buffer, buffer_size, "rcheevos/" RCHEEVOS_VERSION_STRING);
   5994 
   5995   /* some implementations of snprintf will fill the buffer without null terminating.
   5996    * make sure the buffer is null terminated */
   5997   buffer[buffer_size - 1] = '\0';
   5998   return result;
   5999 }