mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-12-02 10:35:39 +00:00
5662 lines
199 KiB
C
5662 lines
199 KiB
C
#include "rc_client_internal.h"
|
|
|
|
#include "rc_api_info.h"
|
|
#include "rc_api_runtime.h"
|
|
#include "rc_api_user.h"
|
|
#include "rc_consoles.h"
|
|
#include "rc_hash.h"
|
|
#include "rc_version.h"
|
|
|
|
#include "rapi/rc_api_common.h"
|
|
|
|
#include "rcheevos/rc_internal.h"
|
|
|
|
#include <stdarg.h>
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#include <profileapi.h>
|
|
#else
|
|
#include <time.h>
|
|
#endif
|
|
|
|
#define RC_CLIENT_UNKNOWN_GAME_ID (uint32_t)-1
|
|
#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */
|
|
|
|
#define RC_MINIMUM_UNPAUSED_FRAMES 20
|
|
#define RC_PAUSE_DECAY_MULTIPLIER 4
|
|
|
|
enum {
|
|
RC_CLIENT_ASYNC_NOT_ABORTED = 0,
|
|
RC_CLIENT_ASYNC_ABORTED = 1,
|
|
RC_CLIENT_ASYNC_DESTROYED = 2
|
|
};
|
|
|
|
typedef struct rc_client_generic_callback_data_t {
|
|
rc_client_t* client;
|
|
rc_client_callback_t callback;
|
|
void* callback_userdata;
|
|
rc_client_async_handle_t async_handle;
|
|
} rc_client_generic_callback_data_t;
|
|
|
|
typedef struct rc_client_pending_media_t
|
|
{
|
|
const char* file_path;
|
|
uint8_t* data;
|
|
size_t data_size;
|
|
rc_client_callback_t callback;
|
|
void* callback_userdata;
|
|
} rc_client_pending_media_t;
|
|
|
|
typedef struct rc_client_load_state_t
|
|
{
|
|
rc_client_t* client;
|
|
rc_client_callback_t callback;
|
|
void* callback_userdata;
|
|
|
|
rc_client_game_info_t* game;
|
|
rc_client_subset_info_t* subset;
|
|
rc_client_game_hash_t* hash;
|
|
|
|
rc_hash_iterator_t hash_iterator;
|
|
rc_client_pending_media_t* pending_media;
|
|
|
|
rc_api_start_session_response_t *start_session_response;
|
|
|
|
rc_client_async_handle_t async_handle;
|
|
|
|
uint8_t progress;
|
|
uint8_t outstanding_requests;
|
|
uint8_t hash_console_id;
|
|
} rc_client_load_state_t;
|
|
|
|
static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_data);
|
|
static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game);
|
|
static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message);
|
|
static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state, const char* hash, const char* file_path);
|
|
static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
|
|
static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset);
|
|
static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game);
|
|
static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when);
|
|
static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
|
|
static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id);
|
|
static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
|
|
|
|
/* ===== Construction/Destruction ===== */
|
|
|
|
static void rc_client_dummy_event_handler(const rc_client_event_t* event, rc_client_t* client)
|
|
{
|
|
(void)event;
|
|
(void)client;
|
|
}
|
|
|
|
rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function, rc_client_server_call_t server_call_function)
|
|
{
|
|
rc_client_t* client = (rc_client_t*)calloc(1, sizeof(rc_client_t));
|
|
if (!client)
|
|
return NULL;
|
|
|
|
client->state.hardcore = 1;
|
|
client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES;
|
|
|
|
client->callbacks.read_memory = read_memory_function;
|
|
client->callbacks.server_call = server_call_function;
|
|
client->callbacks.event_handler = rc_client_dummy_event_handler;
|
|
rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO);
|
|
rc_client_set_get_time_millisecs_function(client, NULL);
|
|
|
|
rc_mutex_init(&client->state.mutex);
|
|
|
|
rc_buffer_init(&client->state.buffer);
|
|
|
|
return client;
|
|
}
|
|
|
|
void rc_client_destroy(rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
|
|
if (client->state.async_handles[i])
|
|
client->state.async_handles[i]->aborted = RC_CLIENT_ASYNC_DESTROYED;
|
|
}
|
|
|
|
if (client->state.load) {
|
|
client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_DESTROYED;
|
|
client->state.load = NULL;
|
|
}
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
rc_client_unload_game(client);
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->destroy)
|
|
client->state.external_client->destroy();
|
|
#endif
|
|
|
|
rc_buffer_destroy(&client->state.buffer);
|
|
|
|
rc_mutex_destroy(&client->state.mutex);
|
|
|
|
free(client);
|
|
}
|
|
|
|
/* ===== Logging ===== */
|
|
|
|
static rc_client_t* g_hash_client = NULL;
|
|
|
|
static void rc_client_log_hash_message(const char* message) {
|
|
rc_client_log_message(g_hash_client, message);
|
|
}
|
|
|
|
void rc_client_log_message(const rc_client_t* client, const char* message)
|
|
{
|
|
if (client->callbacks.log_call)
|
|
client->callbacks.log_call(message, client);
|
|
}
|
|
|
|
static void rc_client_log_message_va(const rc_client_t* client, const char* format, va_list args)
|
|
{
|
|
if (client->callbacks.log_call) {
|
|
char buffer[2048];
|
|
|
|
#ifdef __STDC_WANT_SECURE_LIB__
|
|
vsprintf_s(buffer, sizeof(buffer), format, args);
|
|
#elif __STDC_VERSION__ >= 199901L /* vsnprintf requires c99 */
|
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
|
#else /* c89 doesn't have a size-limited vsprintf function - assume the buffer is large enough */
|
|
vsprintf(buffer, format, args);
|
|
#endif
|
|
|
|
client->callbacks.log_call(buffer, client);
|
|
}
|
|
}
|
|
|
|
#ifdef RC_NO_VARIADIC_MACROS
|
|
|
|
void RC_CLIENT_LOG_ERR_FORMATTED(const rc_client_t* client, const char* format, ...)
|
|
{
|
|
if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_ERROR) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
rc_client_log_message_va(client, format, args);
|
|
va_end(args);
|
|
}
|
|
}
|
|
|
|
void RC_CLIENT_LOG_WARN_FORMATTED(const rc_client_t* client, const char* format, ...)
|
|
{
|
|
if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_WARN) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
rc_client_log_message_va(client, format, args);
|
|
va_end(args);
|
|
}
|
|
}
|
|
|
|
void RC_CLIENT_LOG_INFO_FORMATTED(const rc_client_t* client, const char* format, ...)
|
|
{
|
|
if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
rc_client_log_message_va(client, format, args);
|
|
va_end(args);
|
|
}
|
|
}
|
|
|
|
void RC_CLIENT_LOG_VERBOSE_FORMATTED(const rc_client_t* client, const char* format, ...)
|
|
{
|
|
if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_VERBOSE) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
rc_client_log_message_va(client, format, args);
|
|
va_end(args);
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
void rc_client_log_message_formatted(const rc_client_t* client, const char* format, ...)
|
|
{
|
|
va_list args;
|
|
va_start(args, format);
|
|
rc_client_log_message_va(client, format, args);
|
|
va_end(args);
|
|
}
|
|
|
|
#endif /* RC_NO_VARIADIC_MACROS */
|
|
|
|
void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_callback_t callback)
|
|
{
|
|
client->callbacks.log_call = callback;
|
|
client->state.log_level = callback ? level : RC_CLIENT_LOG_LEVEL_NONE;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->enable_logging)
|
|
client->state.external_client->enable_logging(client, level, callback);
|
|
#endif
|
|
}
|
|
|
|
/* ===== Common ===== */
|
|
|
|
static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client)
|
|
{
|
|
#if defined(CLOCK_MONOTONIC)
|
|
struct timespec now;
|
|
(void)client;
|
|
|
|
if (clock_gettime(CLOCK_MONOTONIC, &now) < 0)
|
|
return 0;
|
|
|
|
/* round nanoseconds to nearest millisecond and add to seconds */
|
|
return ((rc_clock_t)now.tv_sec * 1000 + ((rc_clock_t)now.tv_nsec / 1000000));
|
|
#elif defined(_WIN32)
|
|
static LARGE_INTEGER freq;
|
|
LARGE_INTEGER ticks;
|
|
|
|
(void)client;
|
|
|
|
/* Frequency is the number of ticks per second and is guaranteed to not change. */
|
|
if (!freq.QuadPart) {
|
|
if (!QueryPerformanceFrequency(&freq))
|
|
return 0;
|
|
|
|
/* convert to number of ticks per millisecond to simplify later calculations */
|
|
freq.QuadPart /= 1000;
|
|
}
|
|
|
|
if (!QueryPerformanceCounter(&ticks))
|
|
return 0;
|
|
|
|
return (rc_clock_t)(ticks.QuadPart / freq.QuadPart);
|
|
#else
|
|
const clock_t clock_now = clock();
|
|
|
|
(void)client;
|
|
|
|
if (sizeof(clock_t) == 4) {
|
|
static uint32_t clock_wraps = 0;
|
|
static clock_t last_clock = 0;
|
|
static time_t last_timet = 0;
|
|
const time_t time_now = time(NULL);
|
|
|
|
if (last_timet != 0) {
|
|
const time_t seconds_per_clock_t = (time_t)(((uint64_t)1 << 32) / CLOCKS_PER_SEC);
|
|
if (clock_now < last_clock) {
|
|
/* clock() has wrapped */
|
|
++clock_wraps;
|
|
}
|
|
else if (time_now - last_timet > seconds_per_clock_t) {
|
|
/* it's been long enough that clock() has wrapped and is higher than the last time it was read */
|
|
++clock_wraps;
|
|
}
|
|
}
|
|
|
|
last_timet = time_now;
|
|
last_clock = clock_now;
|
|
|
|
return (rc_clock_t)((((uint64_t)clock_wraps << 32) | clock_now) / (CLOCKS_PER_SEC / 1000));
|
|
}
|
|
else {
|
|
return (rc_clock_t)(clock_now / (CLOCKS_PER_SEC / 1000));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler)
|
|
{
|
|
client->callbacks.get_time_millisecs = handler ? handler : rc_client_clock_get_now_millisecs;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->set_get_time_millisecs)
|
|
client->state.external_client->set_get_time_millisecs(client, handler);
|
|
#endif
|
|
}
|
|
|
|
int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle)
|
|
{
|
|
int aborted;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
aborted = async_handle->aborted;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return aborted;
|
|
}
|
|
|
|
static void rc_client_begin_async(rc_client_t* client, rc_client_async_handle_t* async_handle)
|
|
{
|
|
size_t i;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
|
|
if (!client->state.async_handles[i]) {
|
|
client->state.async_handles[i] = async_handle;
|
|
break;
|
|
}
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
static int rc_client_end_async(rc_client_t* client, rc_client_async_handle_t* async_handle)
|
|
{
|
|
int aborted = async_handle->aborted;
|
|
|
|
/* if client was destroyed, mutex doesn't exist and we don't need to remove the handle from the collection */
|
|
if (aborted != RC_CLIENT_ASYNC_DESTROYED) {
|
|
size_t i;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
|
|
if (client->state.async_handles[i] == async_handle) {
|
|
client->state.async_handles[i] = NULL;
|
|
break;
|
|
}
|
|
}
|
|
aborted = async_handle->aborted;
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
return aborted;
|
|
}
|
|
|
|
void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle)
|
|
{
|
|
if (async_handle && client) {
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->abort_async) {
|
|
client->state.external_client->abort_async(async_handle);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
async_handle->aborted = RC_CLIENT_ASYNC_ABORTED;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
}
|
|
|
|
static int rc_client_async_handle_valid(rc_client_t* client, rc_client_async_handle_t* async_handle)
|
|
{
|
|
int valid = 0;
|
|
size_t i;
|
|
|
|
/* there is a small window of opportunity where the client could have been destroyed before calling
|
|
* this function, but this function assumes the possibility that the handle has been destroyed, so
|
|
* we can't check it for RC_CLIENT_ASYNC_DESTROYED before attempting to scan the client data */
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
for (i = 0; i < sizeof(client->state.async_handles) / sizeof(client->state.async_handles[0]); ++i) {
|
|
if (client->state.async_handles[i] == async_handle) {
|
|
valid = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return valid;
|
|
}
|
|
|
|
static const char* rc_client_server_error_message(int* result, int http_status_code, const rc_api_response_t* response)
|
|
{
|
|
if (!response->succeeded) {
|
|
if (*result == RC_OK) {
|
|
*result = RC_API_FAILURE;
|
|
if (!response->error_message)
|
|
return "Unexpected API failure with no error message";
|
|
}
|
|
|
|
if (response->error_message)
|
|
return response->error_message;
|
|
}
|
|
|
|
(void)http_status_code;
|
|
|
|
if (*result != RC_OK)
|
|
return rc_error_str(*result);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void rc_client_raise_server_error_event(rc_client_t* client,
|
|
const char* api, uint32_t related_id, int result, const char* error_message)
|
|
{
|
|
rc_client_server_error_t server_error;
|
|
rc_client_event_t client_event;
|
|
|
|
server_error.api = api;
|
|
server_error.error_message = error_message;
|
|
server_error.result = result;
|
|
server_error.related_id = related_id;
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
client_event.type = RC_CLIENT_EVENT_SERVER_ERROR;
|
|
client_event.server_error = &server_error;
|
|
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
static void rc_client_update_disconnect_state(rc_client_t* client)
|
|
{
|
|
rc_client_scheduled_callback_data_t* scheduled_callback;
|
|
uint8_t new_state = RC_CLIENT_DISCONNECT_HIDDEN;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
scheduled_callback = client->state.scheduled_callbacks;
|
|
for (; scheduled_callback; scheduled_callback = scheduled_callback->next) {
|
|
if (scheduled_callback->callback == rc_client_award_achievement_retry ||
|
|
scheduled_callback->callback == rc_client_submit_leaderboard_entry_retry) {
|
|
new_state = RC_CLIENT_DISCONNECT_VISIBLE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((client->state.disconnect & RC_CLIENT_DISCONNECT_VISIBLE) != new_state) {
|
|
if (new_state == RC_CLIENT_DISCONNECT_VISIBLE)
|
|
client->state.disconnect = RC_CLIENT_DISCONNECT_HIDDEN | RC_CLIENT_DISCONNECT_SHOW_PENDING;
|
|
else
|
|
client->state.disconnect = RC_CLIENT_DISCONNECT_VISIBLE | RC_CLIENT_DISCONNECT_HIDE_PENDING;
|
|
}
|
|
else {
|
|
client->state.disconnect = new_state;
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
static void rc_client_raise_disconnect_events(rc_client_t* client)
|
|
{
|
|
rc_client_event_t client_event;
|
|
uint8_t new_state;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
if (client->state.disconnect & RC_CLIENT_DISCONNECT_SHOW_PENDING)
|
|
new_state = RC_CLIENT_DISCONNECT_VISIBLE;
|
|
else
|
|
new_state = RC_CLIENT_DISCONNECT_HIDDEN;
|
|
client->state.disconnect = new_state;
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
client_event.type = (new_state == RC_CLIENT_DISCONNECT_VISIBLE) ?
|
|
RC_CLIENT_EVENT_DISCONNECTED : RC_CLIENT_EVENT_RECONNECTED;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
static int rc_client_should_retry(const rc_api_server_response_t* server_response)
|
|
{
|
|
switch (server_response->http_status_code) {
|
|
case 502: /* 502 Bad Gateway */
|
|
/* nginx connection pool full */
|
|
return 1;
|
|
|
|
case 503: /* 503 Service Temporarily Unavailable */
|
|
/* site is in maintenance mode */
|
|
return 1;
|
|
|
|
case 504: /* 504 Gateway Timeout */
|
|
/* timeout between web server and database server */
|
|
return 1;
|
|
|
|
case 429: /* 429 Too Many Requests */
|
|
/* too many unlocks occurred at the same time */
|
|
return 1;
|
|
|
|
case 521: /* 521 Web Server is Down */
|
|
/* cloudfare could not find the server */
|
|
return 1;
|
|
|
|
case 522: /* 522 Connection Timed Out */
|
|
/* timeout connecting to server from cloudfare */
|
|
return 1;
|
|
|
|
case 523: /* 523 Origin is Unreachable */
|
|
/* cloudfare cannot find server */
|
|
return 1;
|
|
|
|
case 524: /* 524 A Timeout Occurred */
|
|
/* connection to server from cloudfare was dropped before request was completed */
|
|
return 1;
|
|
|
|
case 525: /* 525 SSL Handshake Failed */
|
|
/* web server worker connection pool is exhausted */
|
|
return 1;
|
|
|
|
case RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR:
|
|
/* client provided non-HTTP error (explicitly retryable) */
|
|
return 1;
|
|
|
|
case RC_API_SERVER_RESPONSE_CLIENT_ERROR:
|
|
/* client provided non-HTTP error (implicitly non-retryable) */
|
|
return 0;
|
|
|
|
default:
|
|
/* assume any error not handled above where no response was received should be retried */
|
|
if (server_response->body_length == 0 || !server_response->body || !server_response->body[0])
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int rc_client_get_image_url(char buffer[], size_t buffer_size, int image_type, const char* image_name)
|
|
{
|
|
rc_api_fetch_image_request_t image_request;
|
|
rc_api_request_t request;
|
|
int result;
|
|
|
|
if (!buffer)
|
|
return RC_INVALID_STATE;
|
|
|
|
memset(&image_request, 0, sizeof(image_request));
|
|
image_request.image_type = image_type;
|
|
image_request.image_name = image_name;
|
|
result = rc_api_init_fetch_image_request(&request, &image_request);
|
|
if (result == RC_OK)
|
|
snprintf(buffer, buffer_size, "%s", request.url);
|
|
|
|
rc_api_destroy_request(&request);
|
|
return result;
|
|
}
|
|
|
|
/* ===== User ===== */
|
|
|
|
static void rc_client_login_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_generic_callback_data_t* login_callback_data = (rc_client_generic_callback_data_t*)callback_data;
|
|
rc_client_t* client = login_callback_data->client;
|
|
rc_api_login_response_t login_response;
|
|
rc_client_load_state_t* load_state;
|
|
const char* error_message;
|
|
int result;
|
|
|
|
result = rc_client_end_async(client, &login_callback_data->async_handle);
|
|
if (result) {
|
|
if (result != RC_CLIENT_ASYNC_DESTROYED)
|
|
rc_client_logout(client); /* logout will reset the user state and call the load game callback */
|
|
|
|
free(login_callback_data);
|
|
return;
|
|
}
|
|
|
|
if (client->state.user == RC_CLIENT_USER_STATE_NONE) {
|
|
/* logout was called */
|
|
if (login_callback_data->callback)
|
|
login_callback_data->callback(RC_ABORTED, "Login aborted", client, login_callback_data->callback_userdata);
|
|
|
|
free(login_callback_data);
|
|
/* logout call will immediately abort load game before this callback gets called */
|
|
return;
|
|
}
|
|
|
|
result = rc_api_process_login_server_response(&login_response, server_response);
|
|
error_message = rc_client_server_error_message(&result, server_response->http_status_code, &login_response.response);
|
|
if (error_message) {
|
|
rc_mutex_lock(&client->state.mutex);
|
|
client->state.user = RC_CLIENT_USER_STATE_NONE;
|
|
load_state = client->state.load;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
RC_CLIENT_LOG_ERR_FORMATTED(client, "Login failed: %s", error_message);
|
|
if (login_callback_data->callback)
|
|
login_callback_data->callback(result, error_message, client, login_callback_data->callback_userdata);
|
|
|
|
if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
|
|
rc_client_begin_fetch_game_data(load_state);
|
|
}
|
|
else {
|
|
client->user.username = rc_buffer_strcpy(&client->state.buffer, login_response.username);
|
|
|
|
if (strcmp(login_response.username, login_response.display_name) == 0)
|
|
client->user.display_name = client->user.username;
|
|
else
|
|
client->user.display_name = rc_buffer_strcpy(&client->state.buffer, login_response.display_name);
|
|
|
|
client->user.token = rc_buffer_strcpy(&client->state.buffer, login_response.api_token);
|
|
client->user.score = login_response.score;
|
|
client->user.score_softcore = login_response.score_softcore;
|
|
client->user.num_unread_messages = login_response.num_unread_messages;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
client->state.user = RC_CLIENT_USER_STATE_LOGGED_IN;
|
|
load_state = client->state.load;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "%s logged in successfully", login_response.display_name);
|
|
|
|
if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
|
|
rc_client_begin_fetch_game_data(load_state);
|
|
|
|
if (login_callback_data->callback)
|
|
login_callback_data->callback(RC_OK, NULL, client, login_callback_data->callback_userdata);
|
|
}
|
|
|
|
rc_api_destroy_login_response(&login_response);
|
|
free(login_callback_data);
|
|
}
|
|
|
|
static rc_client_async_handle_t* rc_client_begin_login(rc_client_t* client,
|
|
const rc_api_login_request_t* login_request, rc_client_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_client_generic_callback_data_t* callback_data;
|
|
rc_api_request_t request;
|
|
int result = rc_api_init_login_request(&request, login_request);
|
|
const char* error_message = rc_error_str(result);
|
|
|
|
if (result == RC_OK) {
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
if (client->state.user == RC_CLIENT_USER_STATE_LOGIN_REQUESTED) {
|
|
error_message = "Login already in progress";
|
|
result = RC_INVALID_STATE;
|
|
}
|
|
client->state.user = RC_CLIENT_USER_STATE_LOGIN_REQUESTED;
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
if (result != RC_OK) {
|
|
callback(result, error_message, client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
callback_data = (rc_client_generic_callback_data_t*)calloc(1, sizeof(*callback_data));
|
|
if (!callback_data) {
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
callback_data->client = client;
|
|
callback_data->callback = callback;
|
|
callback_data->callback_userdata = callback_userdata;
|
|
|
|
rc_client_begin_async(client, &callback_data->async_handle);
|
|
client->callbacks.server_call(&request, rc_client_login_callback, callback_data, client);
|
|
|
|
rc_api_destroy_request(&request);
|
|
|
|
/* if the user state has changed, the async operation completed synchronously */
|
|
rc_mutex_lock(&client->state.mutex);
|
|
if (client->state.user != RC_CLIENT_USER_STATE_LOGIN_REQUESTED)
|
|
callback_data = NULL;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return callback_data ? &callback_data->async_handle : NULL;
|
|
}
|
|
|
|
rc_client_async_handle_t* rc_client_begin_login_with_password(rc_client_t* client,
|
|
const char* username, const char* password, rc_client_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_api_login_request_t login_request;
|
|
|
|
if (!client) {
|
|
callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (!username || !username[0]) {
|
|
callback(RC_INVALID_STATE, "username is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (!password || !password[0]) {
|
|
callback(RC_INVALID_STATE, "password is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_login_with_password)
|
|
return client->state.external_client->begin_login_with_password(client, username, password, callback, callback_userdata);
|
|
#endif
|
|
|
|
memset(&login_request, 0, sizeof(login_request));
|
|
login_request.username = username;
|
|
login_request.password = password;
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with password)", username);
|
|
return rc_client_begin_login(client, &login_request, callback, callback_userdata);
|
|
}
|
|
|
|
rc_client_async_handle_t* rc_client_begin_login_with_token(rc_client_t* client,
|
|
const char* username, const char* token, rc_client_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_api_login_request_t login_request;
|
|
|
|
if (!client) {
|
|
callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (!username || !username[0]) {
|
|
callback(RC_INVALID_STATE, "username is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (!token || !token[0]) {
|
|
callback(RC_INVALID_STATE, "token is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_login_with_token)
|
|
return client->state.external_client->begin_login_with_token(client, username, token, callback, callback_userdata);
|
|
#endif
|
|
|
|
memset(&login_request, 0, sizeof(login_request));
|
|
login_request.username = username;
|
|
login_request.api_token = token;
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Attempting to log in %s (with token)", username);
|
|
return rc_client_begin_login(client, &login_request, callback, callback_userdata);
|
|
}
|
|
|
|
void rc_client_logout(rc_client_t* client)
|
|
{
|
|
rc_client_load_state_t* load_state;
|
|
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->logout) {
|
|
client->state.external_client->logout();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
switch (client->state.user) {
|
|
case RC_CLIENT_USER_STATE_LOGGED_IN:
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Logging %s out", client->user.display_name);
|
|
break;
|
|
|
|
case RC_CLIENT_USER_STATE_LOGIN_REQUESTED:
|
|
RC_CLIENT_LOG_INFO(client, "Aborting login");
|
|
break;
|
|
}
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
client->state.user = RC_CLIENT_USER_STATE_NONE;
|
|
memset(&client->user, 0, sizeof(client->user));
|
|
|
|
load_state = client->state.load;
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
rc_client_unload_game(client);
|
|
|
|
if (load_state && load_state->progress == RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN)
|
|
rc_client_load_error(load_state, RC_ABORTED, "Login aborted");
|
|
}
|
|
|
|
const rc_client_user_t* rc_client_get_user_info(const rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return NULL;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_user_info)
|
|
return client->state.external_client->get_user_info();
|
|
#endif
|
|
|
|
return (client->state.user == RC_CLIENT_USER_STATE_LOGGED_IN) ? &client->user : NULL;
|
|
}
|
|
|
|
int rc_client_user_get_image_url(const rc_client_user_t* user, char buffer[], size_t buffer_size)
|
|
{
|
|
if (!user)
|
|
return RC_INVALID_STATE;
|
|
|
|
return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, user->display_name);
|
|
}
|
|
|
|
static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t* subset,
|
|
rc_client_user_game_summary_t* summary, const uint8_t unlock_bit)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < stop; ++achievement) {
|
|
switch (achievement->public_.category) {
|
|
case RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE:
|
|
++summary->num_core_achievements;
|
|
summary->points_core += achievement->public_.points;
|
|
|
|
if (achievement->public_.unlocked & unlock_bit) {
|
|
++summary->num_unlocked_achievements;
|
|
summary->points_unlocked += achievement->public_.points;
|
|
}
|
|
if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) {
|
|
++summary->num_unsupported_achievements;
|
|
}
|
|
|
|
break;
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL:
|
|
++summary->num_unofficial_achievements;
|
|
break;
|
|
|
|
default:
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
void rc_client_get_user_game_summary(const rc_client_t* client, rc_client_user_game_summary_t* summary)
|
|
{
|
|
const uint8_t unlock_bit = (client->state.hardcore) ?
|
|
RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
|
|
|
|
if (!summary)
|
|
return;
|
|
|
|
memset(summary, 0, sizeof(*summary));
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_user_game_summary) {
|
|
client->state.external_client->get_user_game_summary(summary);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return;
|
|
|
|
rc_mutex_lock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */
|
|
|
|
rc_client_subset_get_user_game_summary(client->game->subsets, summary, unlock_bit);
|
|
|
|
rc_mutex_unlock((rc_mutex_t*)&client->state.mutex); /* remove const cast for mutex access */
|
|
}
|
|
|
|
/* ===== Game ===== */
|
|
|
|
static void rc_client_free_game(rc_client_game_info_t* game)
|
|
{
|
|
rc_runtime_destroy(&game->runtime);
|
|
|
|
rc_buffer_destroy(&game->buffer);
|
|
|
|
free(game);
|
|
}
|
|
|
|
static void rc_client_free_load_state(rc_client_load_state_t* load_state)
|
|
{
|
|
if (load_state->game)
|
|
rc_client_free_game(load_state->game);
|
|
|
|
if (load_state->start_session_response) {
|
|
rc_api_destroy_start_session_response(load_state->start_session_response);
|
|
free(load_state->start_session_response);
|
|
}
|
|
|
|
free(load_state);
|
|
}
|
|
|
|
static void rc_client_begin_load_state(rc_client_load_state_t* load_state, uint8_t state, uint8_t num_requests)
|
|
{
|
|
rc_mutex_lock(&load_state->client->state.mutex);
|
|
|
|
load_state->progress = state;
|
|
load_state->outstanding_requests += num_requests;
|
|
|
|
rc_mutex_unlock(&load_state->client->state.mutex);
|
|
}
|
|
|
|
static int rc_client_end_load_state(rc_client_load_state_t* load_state)
|
|
{
|
|
int remaining_requests = 0;
|
|
int aborted = 0;
|
|
|
|
rc_mutex_lock(&load_state->client->state.mutex);
|
|
|
|
if (load_state->outstanding_requests > 0)
|
|
--load_state->outstanding_requests;
|
|
remaining_requests = load_state->outstanding_requests;
|
|
|
|
if (load_state->client->state.load != load_state)
|
|
aborted = 1;
|
|
|
|
rc_mutex_unlock(&load_state->client->state.mutex);
|
|
|
|
if (aborted) {
|
|
/* we can't actually free the load_state itself if there are any outstanding requests
|
|
* or their callbacks will try to use the free'd memory. As they call end_load_state,
|
|
* the outstanding_requests count will reach zero and the memory will be free'd then. */
|
|
if (remaining_requests == 0) {
|
|
/* if one of the callbacks called rc_client_load_error, progress will be set to
|
|
* RC_CLIENT_LOAD_STATE_ABORTED. There's no need to call the callback with RC_ABORTED
|
|
* in that case, as it will have already been called with something more appropriate. */
|
|
if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_ABORTED && load_state->callback)
|
|
load_state->callback(RC_ABORTED, "The requested game is no longer active", load_state->client, load_state->callback_userdata);
|
|
|
|
rc_client_free_load_state(load_state);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
return remaining_requests;
|
|
}
|
|
|
|
static void rc_client_load_error(rc_client_load_state_t* load_state, int result, const char* error_message)
|
|
{
|
|
int remaining_requests = 0;
|
|
|
|
rc_mutex_lock(&load_state->client->state.mutex);
|
|
|
|
load_state->progress = RC_CLIENT_LOAD_GAME_STATE_ABORTED;
|
|
if (load_state->client->state.load == load_state)
|
|
load_state->client->state.load = NULL;
|
|
|
|
remaining_requests = load_state->outstanding_requests;
|
|
|
|
rc_mutex_unlock(&load_state->client->state.mutex);
|
|
|
|
RC_CLIENT_LOG_ERR_FORMATTED(load_state->client, "Load failed (%d): %s", result, error_message);
|
|
|
|
if (load_state->callback)
|
|
load_state->callback(result, error_message, load_state->client, load_state->callback_userdata);
|
|
|
|
/* we can't actually free the load_state itself if there are any outstanding requests
|
|
* or their callbacks will try to use the free'd memory. as they call end_load_state,
|
|
* the outstanding_requests count will reach zero and the memory will be free'd then. */
|
|
if (remaining_requests == 0)
|
|
rc_client_free_load_state(load_state);
|
|
}
|
|
|
|
static void rc_client_load_aborted(rc_client_load_state_t* load_state)
|
|
{
|
|
/* prevent callback from being called when manually aborted */
|
|
load_state->callback = NULL;
|
|
|
|
/* mark the game as no longer being loaded */
|
|
rc_client_load_error(load_state, RC_ABORTED, NULL);
|
|
|
|
/* decrement the async counter and potentially free the load_state object */
|
|
rc_client_end_load_state(load_state);
|
|
}
|
|
|
|
static void rc_client_invalidate_memref_achievements(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref)
|
|
{
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_DISABLED)
|
|
continue;
|
|
|
|
if (rc_trigger_contains_memref(achievement->trigger, memref)) {
|
|
achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED;
|
|
achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED;
|
|
|
|
if (achievement->trigger)
|
|
achievement->trigger->state = RC_TRIGGER_STATE_DISABLED;
|
|
|
|
RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled achievement %u. Invalid address %06X", achievement->public_.id, memref->address);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rc_client_invalidate_memref_leaderboards(rc_client_game_info_t* game, rc_client_t* client, rc_memref_t* memref)
|
|
{
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
|
|
rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED)
|
|
continue;
|
|
if (!leaderboard->lboard)
|
|
continue;
|
|
|
|
if (rc_trigger_contains_memref(&leaderboard->lboard->start, memref))
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
|
|
else if (rc_trigger_contains_memref(&leaderboard->lboard->cancel, memref))
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
|
|
else if (rc_trigger_contains_memref(&leaderboard->lboard->submit, memref))
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
|
|
else if (rc_value_contains_memref(&leaderboard->lboard->value, memref))
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
|
|
else
|
|
continue;
|
|
|
|
leaderboard->lboard->state = RC_LBOARD_STATE_DISABLED;
|
|
|
|
RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabled leaderboard %u. Invalid address %06X", leaderboard->public_.id, memref->address);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rc_client_validate_addresses(rc_client_game_info_t* game, rc_client_t* client)
|
|
{
|
|
const rc_memory_regions_t* regions = rc_console_memory_regions(game->public_.console_id);
|
|
const uint32_t max_address = (regions && regions->num_regions > 0) ?
|
|
regions->region[regions->num_regions - 1].end_address : 0xFFFFFFFF;
|
|
uint8_t buffer[8];
|
|
uint32_t total_count = 0;
|
|
uint32_t invalid_count = 0;
|
|
|
|
rc_memref_t** last_memref = &game->runtime.memrefs;
|
|
rc_memref_t* memref = game->runtime.memrefs;
|
|
for (; memref; memref = memref->next) {
|
|
if (!memref->value.is_indirect) {
|
|
total_count++;
|
|
|
|
if (memref->address > max_address ||
|
|
client->callbacks.read_memory(memref->address, buffer, 1, client) == 0) {
|
|
/* invalid address, remove from chain so we don't have to evaluate it in the future.
|
|
* it's still there, so anything referencing it will always fetch 0. */
|
|
*last_memref = memref->next;
|
|
|
|
rc_client_invalidate_memref_achievements(game, client, memref);
|
|
rc_client_invalidate_memref_leaderboards(game, client, memref);
|
|
|
|
invalid_count++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
last_memref = &memref->next;
|
|
}
|
|
|
|
game->max_valid_address = max_address;
|
|
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "%u/%u memory addresses valid", total_count - invalid_count, total_count);
|
|
}
|
|
|
|
static void rc_client_update_legacy_runtime_achievements(rc_client_game_info_t* game, uint32_t active_count)
|
|
{
|
|
if (active_count > 0) {
|
|
rc_client_achievement_info_t* achievement;
|
|
rc_client_achievement_info_t* stop;
|
|
rc_runtime_trigger_t* trigger;
|
|
rc_client_subset_info_t* subset;
|
|
|
|
if (active_count <= game->runtime.trigger_capacity) {
|
|
if (active_count != 0)
|
|
memset(game->runtime.triggers, 0, active_count * sizeof(rc_runtime_trigger_t));
|
|
} else {
|
|
if (game->runtime.triggers)
|
|
free(game->runtime.triggers);
|
|
|
|
game->runtime.trigger_capacity = active_count;
|
|
game->runtime.triggers = (rc_runtime_trigger_t*)calloc(1, active_count * sizeof(rc_runtime_trigger_t));
|
|
}
|
|
|
|
trigger = game->runtime.triggers;
|
|
if (!trigger) {
|
|
/* malloc failed, no way to report error, just bail */
|
|
game->runtime.trigger_count = 0;
|
|
return;
|
|
}
|
|
|
|
for (subset = game->subsets; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
achievement = subset->achievements;
|
|
stop = achievement + subset->public_.num_achievements;
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) {
|
|
trigger->id = achievement->public_.id;
|
|
memcpy(trigger->md5, achievement->md5, 16);
|
|
trigger->trigger = achievement->trigger;
|
|
++trigger;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
game->runtime.trigger_count = active_count;
|
|
}
|
|
|
|
static uint32_t rc_client_subset_count_active_achievements(const rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
uint32_t active_count = 0;
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
|
|
++active_count;
|
|
}
|
|
|
|
return active_count;
|
|
}
|
|
|
|
void rc_client_update_active_achievements(rc_client_game_info_t* game)
|
|
{
|
|
uint32_t active_count = 0;
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (subset->active)
|
|
active_count += rc_client_subset_count_active_achievements(subset);
|
|
}
|
|
|
|
rc_client_update_legacy_runtime_achievements(game, active_count);
|
|
}
|
|
|
|
static uint32_t rc_client_subset_toggle_hardcore_achievements(rc_client_subset_info_t* subset, rc_client_t* client, uint8_t active_bit)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
uint32_t active_count = 0;
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
if ((achievement->public_.unlocked & active_bit) == 0) {
|
|
switch (achievement->public_.state) {
|
|
case RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED:
|
|
case RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE:
|
|
rc_reset_trigger(achievement->trigger);
|
|
achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE;
|
|
++active_count;
|
|
break;
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE:
|
|
++active_count;
|
|
break;
|
|
}
|
|
}
|
|
else if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE ||
|
|
achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_INACTIVE) {
|
|
|
|
/* if it's active despite being unlocked, and we're in encore mode, leave it active */
|
|
if (client->state.encore_mode) {
|
|
++active_count;
|
|
continue;
|
|
}
|
|
|
|
achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED;
|
|
achievement->public_.unlock_time = (active_bit == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) ?
|
|
achievement->unlock_time_hardcore : achievement->unlock_time_softcore;
|
|
|
|
if (achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) {
|
|
rc_client_event_t client_event;
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE;
|
|
client_event.achievement = &achievement->public_;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
if (achievement->trigger && rc_trigger_state_active(achievement->trigger->state))
|
|
achievement->trigger->state = RC_TRIGGER_STATE_TRIGGERED;
|
|
}
|
|
}
|
|
|
|
return active_count;
|
|
}
|
|
|
|
static void rc_client_toggle_hardcore_achievements(rc_client_game_info_t* game, rc_client_t* client, uint8_t active_bit)
|
|
{
|
|
uint32_t active_count = 0;
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (subset->active)
|
|
active_count += rc_client_subset_toggle_hardcore_achievements(subset, client, active_bit);
|
|
}
|
|
|
|
rc_client_update_legacy_runtime_achievements(game, active_count);
|
|
}
|
|
|
|
static void rc_client_activate_achievements(rc_client_game_info_t* game, rc_client_t* client)
|
|
{
|
|
const uint8_t active_bit = (client->state.encore_mode) ?
|
|
RC_CLIENT_ACHIEVEMENT_UNLOCKED_NONE : (client->state.hardcore) ?
|
|
RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
|
|
|
|
rc_client_toggle_hardcore_achievements(game, client, active_bit);
|
|
}
|
|
|
|
static void rc_client_update_legacy_runtime_leaderboards(rc_client_game_info_t* game, uint32_t active_count)
|
|
{
|
|
if (active_count > 0) {
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* stop;
|
|
rc_client_subset_info_t* subset;
|
|
rc_runtime_lboard_t* lboard;
|
|
|
|
if (active_count <= game->runtime.lboard_capacity) {
|
|
if (active_count != 0)
|
|
memset(game->runtime.lboards, 0, active_count * sizeof(rc_runtime_lboard_t));
|
|
} else {
|
|
if (game->runtime.lboards)
|
|
free(game->runtime.lboards);
|
|
|
|
game->runtime.lboard_capacity = active_count;
|
|
game->runtime.lboards = (rc_runtime_lboard_t*)calloc(1, active_count * sizeof(rc_runtime_lboard_t));
|
|
}
|
|
|
|
lboard = game->runtime.lboards;
|
|
if (!lboard) {
|
|
/* malloc failed. no way to report error, just bail */
|
|
game->runtime.lboard_count = 0;
|
|
return;
|
|
}
|
|
|
|
for (subset = game->subsets; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_ACTIVE ||
|
|
leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) {
|
|
lboard->id = leaderboard->public_.id;
|
|
memcpy(lboard->md5, leaderboard->md5, 16);
|
|
lboard->lboard = leaderboard->lboard;
|
|
++lboard;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
game->runtime.lboard_count = active_count;
|
|
}
|
|
|
|
void rc_client_update_active_leaderboards(rc_client_game_info_t* game)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* stop;
|
|
|
|
uint32_t active_count = 0;
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next)
|
|
{
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
|
|
for (; leaderboard < stop; ++leaderboard)
|
|
{
|
|
switch (leaderboard->public_.state)
|
|
{
|
|
case RC_CLIENT_LEADERBOARD_STATE_ACTIVE:
|
|
case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
|
|
++active_count;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
rc_client_update_legacy_runtime_leaderboards(game, active_count);
|
|
}
|
|
|
|
static void rc_client_activate_leaderboards(rc_client_game_info_t* game, rc_client_t* client)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* stop;
|
|
const uint8_t leaderboards_allowed =
|
|
client->state.hardcore || client->state.allow_leaderboards_in_softcore;
|
|
|
|
uint32_t active_count = 0;
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
switch (leaderboard->public_.state) {
|
|
case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
|
|
continue;
|
|
|
|
case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
|
|
if (leaderboards_allowed) {
|
|
rc_reset_lboard(leaderboard->lboard);
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
|
|
++active_count;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (leaderboards_allowed)
|
|
++active_count;
|
|
else
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
rc_client_update_legacy_runtime_leaderboards(game, active_count);
|
|
}
|
|
|
|
static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_client_t* client)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* stop;
|
|
|
|
rc_client_subset_info_t* subset = game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
switch (leaderboard->public_.state) {
|
|
case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
|
|
case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
|
|
continue;
|
|
|
|
case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
|
|
rc_client_release_leaderboard_tracker(client->game, leaderboard);
|
|
/* fallthrough */ /* to default */
|
|
default:
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_INACTIVE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
game->runtime.lboard_count = 0;
|
|
}
|
|
|
|
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)
|
|
{
|
|
rc_client_achievement_info_t* start = subset->achievements;
|
|
rc_client_achievement_info_t* stop = start + subset->public_.num_achievements;
|
|
rc_client_achievement_info_t* scan;
|
|
rc_api_unlock_entry_t* unlock = unlocks;
|
|
rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks;
|
|
|
|
for (; unlock < unlock_stop; ++unlock) {
|
|
for (scan = start; scan < stop; ++scan) {
|
|
if (scan->public_.id == unlock->achievement_id) {
|
|
scan->public_.unlocked |= mode;
|
|
|
|
if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE)
|
|
scan->unlock_time_hardcore = unlock->when;
|
|
if (mode & RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE)
|
|
scan->unlock_time_softcore = unlock->when;
|
|
|
|
if (scan == start)
|
|
++start;
|
|
else if (scan + 1 == stop)
|
|
--stop;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rc_client_activate_game(rc_client_load_state_t* load_state, rc_api_start_session_response_t *start_session_response)
|
|
{
|
|
rc_client_t* client = load_state->client;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
load_state->progress = (client->state.load == load_state) ?
|
|
RC_CLIENT_LOAD_GAME_STATE_DONE : RC_CLIENT_LOAD_GAME_STATE_ABORTED;
|
|
client->state.load = NULL;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (load_state->progress != RC_CLIENT_LOAD_GAME_STATE_DONE) {
|
|
/* previous load state was aborted */
|
|
if (load_state->callback)
|
|
load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
|
|
}
|
|
else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
|
|
/* unlocks not available - assume malloc failed */
|
|
if (load_state->callback)
|
|
load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata);
|
|
}
|
|
else {
|
|
if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
|
|
rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks,
|
|
start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH);
|
|
rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks,
|
|
start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
|
|
}
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
if (client->state.load == NULL)
|
|
client->game = load_state->game;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (client->game != load_state->game) {
|
|
/* previous load state was aborted */
|
|
if (load_state->callback)
|
|
load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
|
|
}
|
|
else {
|
|
/* if a change media request is pending, kick it off */
|
|
rc_client_pending_media_t* pending_media;
|
|
|
|
rc_mutex_lock(&load_state->client->state.mutex);
|
|
pending_media = load_state->pending_media;
|
|
load_state->pending_media = NULL;
|
|
rc_mutex_unlock(&load_state->client->state.mutex);
|
|
|
|
if (pending_media) {
|
|
rc_client_begin_change_media(client, pending_media->file_path,
|
|
pending_media->data, pending_media->data_size, pending_media->callback, pending_media->callback_userdata);
|
|
if (pending_media->data)
|
|
free(pending_media->data);
|
|
free((void*)pending_media->file_path);
|
|
free(pending_media);
|
|
}
|
|
|
|
/* client->game must be set before calling this function so it can query the console_id */
|
|
rc_client_validate_addresses(load_state->game, client);
|
|
|
|
rc_client_activate_achievements(load_state->game, client);
|
|
rc_client_activate_leaderboards(load_state->game, client);
|
|
|
|
if (load_state->hash->hash[0] != '[') {
|
|
if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_LOCKED) {
|
|
/* schedule the periodic ping */
|
|
rc_client_scheduled_callback_data_t* callback_data = rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_scheduled_callback_data_t));
|
|
memset(callback_data, 0, sizeof(*callback_data));
|
|
callback_data->callback = rc_client_ping;
|
|
callback_data->related_id = load_state->game->public_.id;
|
|
callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000;
|
|
rc_client_schedule_callback(client, callback_data);
|
|
}
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcore %s%s", load_state->game->public_.id,
|
|
client->state.hardcore ? "enabled" : "disabled",
|
|
(client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : "");
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Subset %u loaded", load_state->subset->public_.id);
|
|
}
|
|
|
|
if (load_state->callback)
|
|
load_state->callback(RC_OK, NULL, client, load_state->callback_userdata);
|
|
|
|
/* detach the game object so it doesn't get freed by free_load_state */
|
|
load_state->game = NULL;
|
|
}
|
|
}
|
|
|
|
rc_client_free_load_state(load_state);
|
|
}
|
|
|
|
static void rc_client_start_session_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
|
|
rc_api_start_session_response_t start_session_response;
|
|
int outstanding_requests;
|
|
const char* error_message;
|
|
int result;
|
|
|
|
result = rc_client_end_async(load_state->client, &load_state->async_handle);
|
|
if (result) {
|
|
if (result != RC_CLIENT_ASYNC_DESTROYED) {
|
|
rc_client_t* client = load_state->client;
|
|
rc_client_load_aborted(load_state);
|
|
RC_CLIENT_LOG_VERBOSE(client, "Load aborted while starting session");
|
|
} else {
|
|
rc_client_free_load_state(load_state);
|
|
}
|
|
return;
|
|
}
|
|
|
|
result = rc_api_process_start_session_server_response(&start_session_response, server_response);
|
|
error_message = rc_client_server_error_message(&result, server_response->http_status_code, &start_session_response.response);
|
|
outstanding_requests = rc_client_end_load_state(load_state);
|
|
|
|
if (error_message) {
|
|
rc_client_load_error(callback_data, result, error_message);
|
|
}
|
|
else if (outstanding_requests < 0) {
|
|
/* previous load state was aborted, load_state was free'd */
|
|
}
|
|
else if (outstanding_requests == 0) {
|
|
rc_client_activate_game(load_state, &start_session_response);
|
|
}
|
|
else {
|
|
load_state->start_session_response =
|
|
(rc_api_start_session_response_t*)malloc(sizeof(rc_api_start_session_response_t));
|
|
|
|
if (!load_state->start_session_response) {
|
|
rc_client_load_error(callback_data, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
|
|
}
|
|
else {
|
|
/* safer to parse the response again than to try to copy it */
|
|
rc_api_process_start_session_response(load_state->start_session_response, server_response->body);
|
|
}
|
|
}
|
|
|
|
rc_api_destroy_start_session_response(&start_session_response);
|
|
}
|
|
|
|
static void rc_client_begin_start_session(rc_client_load_state_t* load_state)
|
|
{
|
|
rc_api_start_session_request_t start_session_params;
|
|
rc_client_t* client = load_state->client;
|
|
rc_api_request_t start_session_request;
|
|
int result;
|
|
|
|
memset(&start_session_params, 0, sizeof(start_session_params));
|
|
start_session_params.username = client->user.username;
|
|
start_session_params.api_token = client->user.token;
|
|
start_session_params.game_id = load_state->hash->game_id;
|
|
start_session_params.game_hash = load_state->hash->hash;
|
|
start_session_params.hardcore = client->state.hardcore;
|
|
|
|
result = rc_api_init_start_session_request(&start_session_request, &start_session_params);
|
|
if (result != RC_OK) {
|
|
rc_client_load_error(load_state, result, rc_error_str(result));
|
|
}
|
|
else {
|
|
rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1);
|
|
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id);
|
|
rc_client_begin_async(client, &load_state->async_handle);
|
|
client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client);
|
|
rc_api_destroy_request(&start_session_request);
|
|
}
|
|
}
|
|
|
|
static void rc_client_copy_achievements(rc_client_load_state_t* load_state,
|
|
rc_client_subset_info_t* subset,
|
|
const rc_api_achievement_definition_t* achievement_definitions, uint32_t num_achievements)
|
|
{
|
|
const rc_api_achievement_definition_t* read;
|
|
const rc_api_achievement_definition_t* stop;
|
|
rc_client_achievement_info_t* achievements;
|
|
rc_client_achievement_info_t* achievement;
|
|
rc_client_achievement_info_t* scan;
|
|
rc_buffer_t* buffer;
|
|
rc_parse_state_t parse;
|
|
const char* memaddr;
|
|
size_t size;
|
|
int trigger_size;
|
|
|
|
subset->achievements = NULL;
|
|
subset->public_.num_achievements = num_achievements;
|
|
|
|
if (num_achievements == 0)
|
|
return;
|
|
|
|
stop = achievement_definitions + num_achievements;
|
|
|
|
/* if not testing unofficial, filter them out */
|
|
if (!load_state->client->state.unofficial_enabled) {
|
|
for (read = achievement_definitions; read < stop; ++read) {
|
|
if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE)
|
|
--num_achievements;
|
|
}
|
|
|
|
subset->public_.num_achievements = num_achievements;
|
|
|
|
if (num_achievements == 0)
|
|
return;
|
|
}
|
|
|
|
/* preallocate space for achievements */
|
|
size = 24 /* assume average title length of 24 */
|
|
+ 48 /* assume average description length of 48 */
|
|
+ sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2 /* trigger container */
|
|
+ sizeof(rc_condition_t) * 8 /* assume average trigger length of 8 conditions */
|
|
+ sizeof(rc_client_achievement_info_t);
|
|
rc_buffer_reserve(&load_state->game->buffer, size * num_achievements);
|
|
|
|
/* allocate the achievement array */
|
|
size = sizeof(rc_client_achievement_info_t) * num_achievements;
|
|
buffer = &load_state->game->buffer;
|
|
achievement = achievements = rc_buffer_alloc(buffer, size);
|
|
memset(achievements, 0, size);
|
|
|
|
/* copy the achievement data */
|
|
for (read = achievement_definitions; read < stop; ++read) {
|
|
if (read->category != RC_ACHIEVEMENT_CATEGORY_CORE && !load_state->client->state.unofficial_enabled)
|
|
continue;
|
|
|
|
achievement->public_.title = rc_buffer_strcpy(buffer, read->title);
|
|
achievement->public_.description = rc_buffer_strcpy(buffer, read->description);
|
|
snprintf(achievement->public_.badge_name, sizeof(achievement->public_.badge_name), "%s", read->badge_name);
|
|
achievement->public_.id = read->id;
|
|
achievement->public_.points = read->points;
|
|
achievement->public_.category = (read->category != RC_ACHIEVEMENT_CATEGORY_CORE) ?
|
|
RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE;
|
|
achievement->public_.rarity = read->rarity;
|
|
achievement->public_.rarity_hardcore = read->rarity_hardcore;
|
|
achievement->public_.type = read->type; /* assert: mapping is 1:1 */
|
|
|
|
memaddr = read->definition;
|
|
rc_runtime_checksum(memaddr, achievement->md5);
|
|
|
|
trigger_size = rc_trigger_size(memaddr);
|
|
if (trigger_size < 0) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", trigger_size, read->id);
|
|
achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED;
|
|
achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED;
|
|
}
|
|
else {
|
|
/* populate the item, using the communal memrefs pool */
|
|
rc_init_parse_state(&parse, rc_buffer_reserve(buffer, trigger_size), NULL, 0);
|
|
parse.first_memref = &load_state->game->runtime.memrefs;
|
|
parse.variables = &load_state->game->runtime.variables;
|
|
achievement->trigger = RC_ALLOC(rc_trigger_t, &parse);
|
|
rc_parse_trigger_internal(achievement->trigger, &memaddr, &parse);
|
|
|
|
if (parse.offset < 0) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing achievement %u", parse.offset, read->id);
|
|
achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_DISABLED;
|
|
achievement->public_.bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED;
|
|
}
|
|
else {
|
|
rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset);
|
|
achievement->trigger->memrefs = NULL; /* memrefs managed by runtime */
|
|
}
|
|
|
|
rc_destroy_parse_state(&parse);
|
|
}
|
|
|
|
achievement->created_time = read->created;
|
|
achievement->updated_time = read->updated;
|
|
|
|
scan = achievement;
|
|
while (scan > achievements) {
|
|
--scan;
|
|
if (strcmp(scan->author, read->author) == 0) {
|
|
achievement->author = scan->author;
|
|
break;
|
|
}
|
|
}
|
|
if (!achievement->author)
|
|
achievement->author = rc_buffer_strcpy(buffer, read->author);
|
|
|
|
++achievement;
|
|
}
|
|
|
|
subset->achievements = achievements;
|
|
}
|
|
|
|
uint8_t rc_client_map_leaderboard_format(int format)
|
|
{
|
|
switch (format) {
|
|
case RC_FORMAT_SECONDS:
|
|
case RC_FORMAT_CENTISECS:
|
|
case RC_FORMAT_MINUTES:
|
|
case RC_FORMAT_SECONDS_AS_MINUTES:
|
|
case RC_FORMAT_FRAMES:
|
|
return RC_CLIENT_LEADERBOARD_FORMAT_TIME;
|
|
|
|
case RC_FORMAT_SCORE:
|
|
return RC_CLIENT_LEADERBOARD_FORMAT_SCORE;
|
|
|
|
case RC_FORMAT_VALUE:
|
|
case RC_FORMAT_FLOAT1:
|
|
case RC_FORMAT_FLOAT2:
|
|
case RC_FORMAT_FLOAT3:
|
|
case RC_FORMAT_FLOAT4:
|
|
case RC_FORMAT_FLOAT5:
|
|
case RC_FORMAT_FLOAT6:
|
|
case RC_FORMAT_FIXED1:
|
|
case RC_FORMAT_FIXED2:
|
|
case RC_FORMAT_FIXED3:
|
|
case RC_FORMAT_TENS:
|
|
case RC_FORMAT_HUNDREDS:
|
|
case RC_FORMAT_THOUSANDS:
|
|
case RC_FORMAT_UNSIGNED_VALUE:
|
|
default:
|
|
return RC_CLIENT_LEADERBOARD_FORMAT_VALUE;
|
|
}
|
|
}
|
|
|
|
static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state,
|
|
rc_client_subset_info_t* subset,
|
|
const rc_api_leaderboard_definition_t* leaderboard_definitions, uint32_t num_leaderboards)
|
|
{
|
|
const rc_api_leaderboard_definition_t* read;
|
|
const rc_api_leaderboard_definition_t* stop;
|
|
rc_client_leaderboard_info_t* leaderboards;
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_buffer_t* buffer;
|
|
rc_parse_state_t parse;
|
|
const char* memaddr;
|
|
const char* ptr;
|
|
size_t size;
|
|
int lboard_size;
|
|
|
|
subset->leaderboards = NULL;
|
|
subset->public_.num_leaderboards = num_leaderboards;
|
|
|
|
if (num_leaderboards == 0)
|
|
return;
|
|
|
|
/* preallocate space for achievements */
|
|
size = 24 /* assume average title length of 24 */
|
|
+ 48 /* assume average description length of 48 */
|
|
+ sizeof(rc_lboard_t) /* lboard container */
|
|
+ (sizeof(rc_trigger_t) + sizeof(rc_condset_t) * 2) * 3 /* start/submit/cancel */
|
|
+ (sizeof(rc_value_t) + sizeof(rc_condset_t)) /* value */
|
|
+ sizeof(rc_condition_t) * 4 * 4 /* assume average of 4 conditions in each start/submit/cancel/value */
|
|
+ sizeof(rc_client_leaderboard_info_t);
|
|
rc_buffer_reserve(&load_state->game->buffer, size * num_leaderboards);
|
|
|
|
/* allocate the achievement array */
|
|
size = sizeof(rc_client_leaderboard_info_t) * num_leaderboards;
|
|
buffer = &load_state->game->buffer;
|
|
leaderboard = leaderboards = rc_buffer_alloc(buffer, size);
|
|
memset(leaderboards, 0, size);
|
|
|
|
/* copy the achievement data */
|
|
read = leaderboard_definitions;
|
|
stop = read + num_leaderboards;
|
|
do {
|
|
leaderboard->public_.title = rc_buffer_strcpy(buffer, read->title);
|
|
leaderboard->public_.description = rc_buffer_strcpy(buffer, read->description);
|
|
leaderboard->public_.id = read->id;
|
|
leaderboard->public_.format = rc_client_map_leaderboard_format(read->format);
|
|
leaderboard->public_.lower_is_better = read->lower_is_better;
|
|
leaderboard->format = (uint8_t)read->format;
|
|
leaderboard->hidden = (uint8_t)read->hidden;
|
|
|
|
memaddr = read->definition;
|
|
rc_runtime_checksum(memaddr, leaderboard->md5);
|
|
|
|
ptr = strstr(memaddr, "VAL:");
|
|
if (ptr != NULL) {
|
|
/* calculate the DJB2 hash of the VAL portion of the string*/
|
|
uint32_t hash = 5381;
|
|
ptr += 4; /* skip 'VAL:' */
|
|
while (*ptr && (ptr[0] != ':' || ptr[1] != ':'))
|
|
hash = (hash << 5) + hash + *ptr++;
|
|
leaderboard->value_djb2 = hash;
|
|
}
|
|
|
|
lboard_size = rc_lboard_size(memaddr);
|
|
if (lboard_size < 0) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", lboard_size, read->id);
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
|
|
}
|
|
else {
|
|
/* populate the item, using the communal memrefs pool */
|
|
rc_init_parse_state(&parse, rc_buffer_reserve(buffer, lboard_size), NULL, 0);
|
|
parse.first_memref = &load_state->game->runtime.memrefs;
|
|
parse.variables = &load_state->game->runtime.variables;
|
|
leaderboard->lboard = RC_ALLOC(rc_lboard_t, &parse);
|
|
rc_parse_lboard_internal(leaderboard->lboard, memaddr, &parse);
|
|
|
|
if (parse.offset < 0) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing leaderboard %u", parse.offset, read->id);
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_DISABLED;
|
|
}
|
|
else {
|
|
rc_buffer_consume(buffer, parse.buffer, (uint8_t*)parse.buffer + parse.offset);
|
|
leaderboard->lboard->memrefs = NULL; /* memrefs managed by runtime */
|
|
}
|
|
|
|
rc_destroy_parse_state(&parse);
|
|
}
|
|
|
|
++leaderboard;
|
|
++read;
|
|
} while (read < stop);
|
|
|
|
subset->leaderboards = leaderboards;
|
|
}
|
|
|
|
static const char* rc_client_subset_extract_title(rc_client_game_info_t* game, const char* title)
|
|
{
|
|
const char* subset_prefix = strstr(title, "[Subset - ");
|
|
if (subset_prefix) {
|
|
const char* start = subset_prefix + 10;
|
|
const char* stop = strstr(start, "]");
|
|
const size_t len = stop - start;
|
|
char* result = (char*)rc_buffer_alloc(&game->buffer, len + 1);
|
|
|
|
memcpy(result, start, len);
|
|
result[len] = '\0';
|
|
return result;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
|
|
rc_api_fetch_game_data_response_t fetch_game_data_response;
|
|
int outstanding_requests;
|
|
const char* error_message;
|
|
int result;
|
|
|
|
result = rc_client_end_async(load_state->client, &load_state->async_handle);
|
|
if (result) {
|
|
if (result != RC_CLIENT_ASYNC_DESTROYED) {
|
|
rc_client_t* client = load_state->client;
|
|
rc_client_load_aborted(load_state);
|
|
RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching game data");
|
|
} else {
|
|
rc_client_free_load_state(load_state);
|
|
}
|
|
return;
|
|
}
|
|
|
|
result = rc_api_process_fetch_game_data_server_response(&fetch_game_data_response, server_response);
|
|
error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_game_data_response.response);
|
|
|
|
outstanding_requests = rc_client_end_load_state(load_state);
|
|
|
|
if (error_message) {
|
|
rc_client_load_error(load_state, result, error_message);
|
|
}
|
|
else if (outstanding_requests < 0) {
|
|
/* previous load state was aborted, load_state was free'd */
|
|
}
|
|
else {
|
|
rc_client_subset_info_t* subset;
|
|
|
|
subset = (rc_client_subset_info_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(rc_client_subset_info_t));
|
|
memset(subset, 0, sizeof(*subset));
|
|
subset->public_.id = fetch_game_data_response.id;
|
|
subset->active = 1;
|
|
snprintf(subset->public_.badge_name, sizeof(subset->public_.badge_name), "%s", fetch_game_data_response.image_name);
|
|
load_state->subset = subset;
|
|
|
|
if (load_state->game->public_.console_id != RC_CONSOLE_UNKNOWN &&
|
|
fetch_game_data_response.console_id != load_state->game->public_.console_id) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Data for game %u is for console %u, expecting console %u",
|
|
fetch_game_data_response.id, fetch_game_data_response.console_id, load_state->game->public_.console_id);
|
|
}
|
|
|
|
/* kick off the start session request while we process the game data */
|
|
rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION, 1);
|
|
if (load_state->client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
|
|
/* we can't unlock achievements without a session, lock spectator mode for the game */
|
|
load_state->client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_LOCKED;
|
|
}
|
|
else {
|
|
rc_client_begin_start_session(load_state);
|
|
}
|
|
|
|
/* process the game data */
|
|
rc_client_copy_achievements(load_state, subset,
|
|
fetch_game_data_response.achievements, fetch_game_data_response.num_achievements);
|
|
rc_client_copy_leaderboards(load_state, subset,
|
|
fetch_game_data_response.leaderboards, fetch_game_data_response.num_leaderboards);
|
|
|
|
if (!load_state->game->subsets) {
|
|
/* core set */
|
|
rc_mutex_lock(&load_state->client->state.mutex);
|
|
load_state->game->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title);
|
|
load_state->game->subsets = subset;
|
|
load_state->game->public_.badge_name = subset->public_.badge_name;
|
|
load_state->game->public_.console_id = fetch_game_data_response.console_id;
|
|
rc_mutex_unlock(&load_state->client->state.mutex);
|
|
|
|
subset->public_.title = load_state->game->public_.title;
|
|
|
|
if (fetch_game_data_response.rich_presence_script && fetch_game_data_response.rich_presence_script[0]) {
|
|
result = rc_runtime_activate_richpresence(&load_state->game->runtime, fetch_game_data_response.rich_presence_script, NULL, 0);
|
|
if (result != RC_OK) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(load_state->client, "Parse error %d processing rich presence", result);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
rc_client_subset_info_t* scan;
|
|
|
|
/* subset - extract subset title */
|
|
subset->public_.title = rc_client_subset_extract_title(load_state->game, fetch_game_data_response.title);
|
|
if (!subset->public_.title) {
|
|
const char* core_subset_title = rc_client_subset_extract_title(load_state->game, load_state->game->public_.title);
|
|
if (core_subset_title) {
|
|
rc_client_subset_info_t* scan = load_state->game->subsets;
|
|
for (; scan; scan = scan->next) {
|
|
if (scan->public_.title == load_state->game->public_.title) {
|
|
scan->public_.title = core_subset_title;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
subset->public_.title = rc_buffer_strcpy(&load_state->game->buffer, fetch_game_data_response.title);
|
|
}
|
|
|
|
/* append to subset list */
|
|
scan = load_state->game->subsets;
|
|
while (scan->next)
|
|
scan = scan->next;
|
|
scan->next = subset;
|
|
}
|
|
|
|
if (load_state->client->callbacks.post_process_game_data_response) {
|
|
load_state->client->callbacks.post_process_game_data_response(server_response,
|
|
&fetch_game_data_response, load_state->client, load_state->callback_userdata);
|
|
}
|
|
|
|
outstanding_requests = rc_client_end_load_state(load_state);
|
|
if (outstanding_requests < 0) {
|
|
/* previous load state was aborted, load_state was free'd */
|
|
}
|
|
else {
|
|
if (outstanding_requests == 0)
|
|
rc_client_activate_game(load_state, load_state->start_session_response);
|
|
}
|
|
}
|
|
|
|
rc_api_destroy_fetch_game_data_response(&fetch_game_data_response);
|
|
}
|
|
|
|
static void rc_client_begin_fetch_game_data(rc_client_load_state_t* load_state)
|
|
{
|
|
rc_api_fetch_game_data_request_t fetch_game_data_request;
|
|
rc_client_t* client = load_state->client;
|
|
rc_api_request_t request;
|
|
int result;
|
|
|
|
if (load_state->hash->game_id == 0) {
|
|
char hash[33];
|
|
|
|
if (rc_hash_iterate(hash, &load_state->hash_iterator)) {
|
|
/* found another hash to try */
|
|
load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1];
|
|
rc_client_load_game(load_state, hash, NULL);
|
|
return;
|
|
}
|
|
|
|
if (load_state->game->media_hash &&
|
|
load_state->game->media_hash->game_hash &&
|
|
load_state->game->media_hash->game_hash->next) {
|
|
/* multiple hashes were tried, create a CSV */
|
|
struct rc_client_game_hash_t* game_hash = load_state->game->media_hash->game_hash;
|
|
int count = 1;
|
|
char* ptr;
|
|
size_t size;
|
|
|
|
size = strlen(game_hash->hash) + 1;
|
|
while (game_hash->next) {
|
|
game_hash = game_hash->next;
|
|
size += strlen(game_hash->hash) + 1;
|
|
count++;
|
|
}
|
|
|
|
ptr = (char*)rc_buffer_alloc(&load_state->game->buffer, size);
|
|
ptr += size - 1;
|
|
*ptr = '\0';
|
|
game_hash = load_state->game->media_hash->game_hash;
|
|
do {
|
|
const size_t hash_len = strlen(game_hash->hash);
|
|
ptr -= hash_len;
|
|
memcpy(ptr, game_hash->hash, hash_len);
|
|
|
|
game_hash = game_hash->next;
|
|
if (!game_hash)
|
|
break;
|
|
|
|
ptr--;
|
|
*ptr = ',';
|
|
} while (1);
|
|
|
|
load_state->game->public_.hash = ptr;
|
|
load_state->game->public_.console_id = RC_CONSOLE_UNKNOWN;
|
|
} else {
|
|
/* only a single hash was tried, capture it */
|
|
load_state->game->public_.console_id = load_state->hash_console_id;
|
|
load_state->game->public_.hash = load_state->hash->hash;
|
|
|
|
if (client->callbacks.identify_unknown_hash) {
|
|
load_state->hash->game_id = client->callbacks.identify_unknown_hash(
|
|
load_state->hash_console_id, load_state->hash->hash, client, load_state->callback_userdata);
|
|
|
|
if (load_state->hash->game_id != 0) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(load_state->client, "Client says to load game %u for unidentified hash %s",
|
|
load_state->hash->game_id, load_state->hash->hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (load_state->hash->game_id == 0) {
|
|
load_state->game->public_.title = "Unknown Game";
|
|
load_state->game->public_.badge_name = "";
|
|
client->game = load_state->game;
|
|
load_state->game = NULL;
|
|
|
|
rc_client_load_error(load_state, RC_NO_GAME_LOADED, "Unknown game");
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (load_state->hash->hash[0] != '[') { /* not [NO HASH] or [SUBSETxx] */
|
|
load_state->game->public_.id = load_state->hash->game_id;
|
|
load_state->game->public_.hash = load_state->hash->hash;
|
|
}
|
|
|
|
/* done with the hashing code, release the global pointer */
|
|
g_hash_client = NULL;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
result = client->state.user;
|
|
if (result == RC_CLIENT_USER_STATE_LOGIN_REQUESTED)
|
|
load_state->progress = RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
switch (result) {
|
|
case RC_CLIENT_USER_STATE_LOGGED_IN:
|
|
break;
|
|
|
|
case RC_CLIENT_USER_STATE_LOGIN_REQUESTED:
|
|
/* do nothing, this function will be called again after login completes */
|
|
return;
|
|
|
|
default:
|
|
rc_client_load_error(load_state, RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED));
|
|
return;
|
|
}
|
|
|
|
memset(&fetch_game_data_request, 0, sizeof(fetch_game_data_request));
|
|
fetch_game_data_request.username = client->user.username;
|
|
fetch_game_data_request.api_token = client->user.token;
|
|
fetch_game_data_request.game_id = load_state->hash->game_id;
|
|
|
|
result = rc_api_init_fetch_game_data_request(&request, &fetch_game_data_request);
|
|
if (result != RC_OK) {
|
|
rc_client_load_error(load_state, result, rc_error_str(result));
|
|
return;
|
|
}
|
|
|
|
rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_FETCHING_GAME_DATA, 1);
|
|
|
|
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Fetching data for game %u", fetch_game_data_request.game_id);
|
|
rc_client_begin_async(client, &load_state->async_handle);
|
|
client->callbacks.server_call(&request, rc_client_fetch_game_data_callback, load_state, client);
|
|
|
|
rc_api_destroy_request(&request);
|
|
}
|
|
|
|
static void rc_client_identify_game_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
|
|
rc_client_t* client = load_state->client;
|
|
rc_api_resolve_hash_response_t resolve_hash_response;
|
|
int outstanding_requests;
|
|
const char* error_message;
|
|
int result;
|
|
|
|
result = rc_client_end_async(client, &load_state->async_handle);
|
|
if (result) {
|
|
if (result != RC_CLIENT_ASYNC_DESTROYED) {
|
|
rc_client_load_aborted(load_state);
|
|
RC_CLIENT_LOG_VERBOSE(client, "Load aborted during game identification");
|
|
} else {
|
|
rc_client_free_load_state(load_state);
|
|
}
|
|
return;
|
|
}
|
|
|
|
result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response);
|
|
error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response);
|
|
|
|
if (error_message) {
|
|
rc_client_end_load_state(load_state);
|
|
rc_client_load_error(load_state, result, error_message);
|
|
}
|
|
else {
|
|
/* hash exists outside the load state - always update it */
|
|
load_state->hash->game_id = resolve_hash_response.game_id;
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash);
|
|
|
|
/* have to call end_load_state after updating hash in case the load_state gets free'd */
|
|
outstanding_requests = rc_client_end_load_state(load_state);
|
|
if (outstanding_requests < 0) {
|
|
/* previous load state was aborted, load_state was free'd */
|
|
}
|
|
else {
|
|
rc_client_begin_fetch_game_data(load_state);
|
|
}
|
|
}
|
|
|
|
rc_api_destroy_resolve_hash_response(&resolve_hash_response);
|
|
}
|
|
|
|
rc_client_game_hash_t* rc_client_find_game_hash(rc_client_t* client, const char* hash)
|
|
{
|
|
rc_client_game_hash_t* game_hash;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
game_hash = client->hashes;
|
|
while (game_hash) {
|
|
if (strcasecmp(game_hash->hash, hash) == 0)
|
|
break;
|
|
|
|
game_hash = game_hash->next;
|
|
}
|
|
|
|
if (!game_hash) {
|
|
game_hash = rc_buffer_alloc(&client->state.buffer, sizeof(rc_client_game_hash_t));
|
|
memset(game_hash, 0, sizeof(*game_hash));
|
|
snprintf(game_hash->hash, sizeof(game_hash->hash), "%s", hash);
|
|
game_hash->game_id = RC_CLIENT_UNKNOWN_GAME_ID;
|
|
game_hash->next = client->hashes;
|
|
client->hashes = game_hash;
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return game_hash;
|
|
}
|
|
|
|
static rc_client_async_handle_t* rc_client_load_game(rc_client_load_state_t* load_state,
|
|
const char* hash, const char* file_path)
|
|
{
|
|
rc_client_t* client = load_state->client;
|
|
rc_client_game_hash_t* old_hash;
|
|
|
|
if (client->state.load == NULL) {
|
|
rc_client_unload_game(client);
|
|
client->state.load = load_state;
|
|
|
|
if (load_state->game == NULL) {
|
|
load_state->game = (rc_client_game_info_t*)calloc(1, sizeof(*load_state->game));
|
|
if (!load_state->game) {
|
|
if (load_state->callback)
|
|
load_state->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, load_state->callback_userdata);
|
|
|
|
rc_client_free_load_state(load_state);
|
|
return NULL;
|
|
}
|
|
|
|
rc_buffer_init(&load_state->game->buffer);
|
|
rc_runtime_init(&load_state->game->runtime);
|
|
}
|
|
}
|
|
else if (client->state.load != load_state) {
|
|
/* previous load was aborted */
|
|
if (load_state->callback)
|
|
load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
|
|
|
|
rc_client_free_load_state(load_state);
|
|
return NULL;
|
|
}
|
|
|
|
old_hash = load_state->hash;
|
|
load_state->hash = rc_client_find_game_hash(client, hash);
|
|
|
|
if (file_path) {
|
|
rc_client_media_hash_t* media_hash =
|
|
(rc_client_media_hash_t*)rc_buffer_alloc(&load_state->game->buffer, sizeof(*media_hash));
|
|
media_hash->game_hash = load_state->hash;
|
|
media_hash->path_djb2 = rc_djb2(file_path);
|
|
media_hash->next = load_state->game->media_hash;
|
|
load_state->game->media_hash = media_hash;
|
|
}
|
|
else if (load_state->game->media_hash && load_state->game->media_hash->game_hash == old_hash) {
|
|
load_state->game->media_hash->game_hash = load_state->hash;
|
|
}
|
|
|
|
if (load_state->hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) {
|
|
rc_api_resolve_hash_request_t resolve_hash_request;
|
|
rc_api_request_t request;
|
|
int result;
|
|
|
|
memset(&resolve_hash_request, 0, sizeof(resolve_hash_request));
|
|
resolve_hash_request.game_hash = hash;
|
|
|
|
result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request);
|
|
if (result != RC_OK) {
|
|
rc_client_load_error(load_state, result, rc_error_str(result));
|
|
return NULL;
|
|
}
|
|
|
|
rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME, 1);
|
|
|
|
rc_client_begin_async(client, &load_state->async_handle);
|
|
client->callbacks.server_call(&request, rc_client_identify_game_callback, load_state, client);
|
|
|
|
rc_api_destroy_request(&request);
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash);
|
|
|
|
rc_client_begin_fetch_game_data(load_state);
|
|
}
|
|
|
|
return (client->state.load == load_state) ? &load_state->async_handle : NULL;
|
|
}
|
|
|
|
rc_hash_iterator_t* rc_client_get_load_state_hash_iterator(rc_client_t* client)
|
|
{
|
|
if (client && client->state.load)
|
|
return &client->state.load->hash_iterator;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
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)
|
|
{
|
|
rc_client_load_state_t* load_state;
|
|
|
|
if (!client) {
|
|
callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (!hash || !hash[0]) {
|
|
callback(RC_INVALID_STATE, "hash is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_load_game)
|
|
return client->state.external_client->begin_load_game(client, hash, callback, callback_userdata);
|
|
#endif
|
|
|
|
load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
|
|
if (!load_state) {
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
load_state->client = client;
|
|
load_state->callback = callback;
|
|
load_state->callback_userdata = callback_userdata;
|
|
|
|
return rc_client_load_game(load_state, hash, NULL);
|
|
}
|
|
|
|
rc_client_async_handle_t* rc_client_begin_identify_and_load_game(rc_client_t* client,
|
|
uint32_t console_id, const char* file_path,
|
|
const uint8_t* data, size_t data_size,
|
|
rc_client_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_client_load_state_t* load_state;
|
|
char hash[33];
|
|
|
|
if (!client) {
|
|
callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_identify_and_load_game)
|
|
return client->state.external_client->begin_identify_and_load_game(client, console_id, file_path, data, data_size, callback, callback_userdata);
|
|
#endif
|
|
|
|
if (data) {
|
|
if (file_path) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p (%s)", data_size, data, file_path);
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %zu bytes at %p", data_size, data);
|
|
}
|
|
}
|
|
else if (file_path) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Identifying game: %s", file_path);
|
|
}
|
|
else {
|
|
callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) {
|
|
g_hash_client = client;
|
|
rc_hash_init_error_message_callback(rc_client_log_hash_message);
|
|
rc_hash_init_verbose_message_callback(rc_client_log_hash_message);
|
|
}
|
|
|
|
if (!file_path)
|
|
file_path = "?";
|
|
|
|
load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
|
|
if (!load_state) {
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
load_state->client = client;
|
|
load_state->callback = callback;
|
|
load_state->callback_userdata = callback_userdata;
|
|
|
|
if (console_id == RC_CONSOLE_UNKNOWN) {
|
|
rc_hash_initialize_iterator(&load_state->hash_iterator, file_path, data, data_size);
|
|
|
|
if (!rc_hash_iterate(hash, &load_state->hash_iterator)) {
|
|
rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed");
|
|
return NULL;
|
|
}
|
|
|
|
load_state->hash_console_id = load_state->hash_iterator.consoles[load_state->hash_iterator.index - 1];
|
|
}
|
|
else {
|
|
/* ASSERT: hash_iterator->index and hash_iterator->consoles[0] will be 0 from calloc */
|
|
load_state->hash_console_id = console_id;
|
|
|
|
if (data != NULL) {
|
|
if (!rc_hash_generate_from_buffer(hash, console_id, data, data_size)) {
|
|
rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed");
|
|
return NULL;
|
|
}
|
|
}
|
|
else {
|
|
if (!rc_hash_generate_from_file(hash, console_id, file_path)) {
|
|
rc_client_load_error(load_state, RC_INVALID_STATE, "hash generation failed");
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
return rc_client_load_game(load_state, hash, file_path);
|
|
}
|
|
|
|
int rc_client_get_load_game_state(const rc_client_t* client)
|
|
{
|
|
int state = RC_CLIENT_LOAD_GAME_STATE_NONE;
|
|
if (client) {
|
|
const rc_client_load_state_t* load_state = client->state.load;
|
|
if (load_state)
|
|
state = load_state->progress;
|
|
else if (client->game)
|
|
state = RC_CLIENT_LOAD_GAME_STATE_DONE;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
static void rc_client_game_mark_ui_to_be_hidden(rc_client_t* client, rc_client_game_info_t* game)
|
|
{
|
|
rc_client_achievement_info_t* achievement;
|
|
rc_client_achievement_info_t* achievement_stop;
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* leaderboard_stop;
|
|
rc_client_subset_info_t* subset;
|
|
|
|
for (subset = game->subsets; subset; subset = subset->next) {
|
|
achievement = subset->achievements;
|
|
achievement_stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < achievement_stop; ++achievement) {
|
|
if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE &&
|
|
achievement->trigger && achievement->trigger->state == RC_TRIGGER_STATE_PRIMED) {
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
|
|
}
|
|
}
|
|
|
|
leaderboard = subset->leaderboards;
|
|
leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < leaderboard_stop; ++leaderboard) {
|
|
if (leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING)
|
|
rc_client_release_leaderboard_tracker(game, leaderboard);
|
|
}
|
|
}
|
|
|
|
rc_client_hide_progress_tracker(client, game);
|
|
}
|
|
|
|
void rc_client_unload_game(rc_client_t* client)
|
|
{
|
|
rc_client_game_info_t* game;
|
|
rc_client_scheduled_callback_data_t** last;
|
|
rc_client_scheduled_callback_data_t* next;
|
|
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->unload_game) {
|
|
client->state.external_client->unload_game();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
game = client->game;
|
|
client->game = NULL;
|
|
|
|
if (client->state.load) {
|
|
/* this mimics rc_client_abort_async without nesting the lock */
|
|
client->state.load->async_handle.aborted = RC_CLIENT_ASYNC_ABORTED;
|
|
client->state.load = NULL;
|
|
}
|
|
|
|
if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED)
|
|
client->state.spectator_mode = RC_CLIENT_SPECTATOR_MODE_ON;
|
|
|
|
if (game != NULL)
|
|
rc_client_game_mark_ui_to_be_hidden(client, game);
|
|
|
|
last = &client->state.scheduled_callbacks;
|
|
do {
|
|
next = *last;
|
|
if (!next)
|
|
break;
|
|
|
|
/* remove rich presence ping scheduled event for game */
|
|
if (next->callback == rc_client_ping && game && next->related_id == game->public_.id) {
|
|
*last = next->next;
|
|
continue;
|
|
}
|
|
|
|
last = &next->next;
|
|
} while (1);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (game != NULL) {
|
|
rc_client_raise_pending_events(client, game);
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Unloading game %u", game->public_.id);
|
|
rc_client_free_game(game);
|
|
}
|
|
}
|
|
|
|
static void rc_client_change_media(rc_client_t* client, const rc_client_game_hash_t* game_hash, rc_client_callback_t callback, void* callback_userdata)
|
|
{
|
|
if (game_hash->game_id == client->game->public_.id) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to valid media for game %u: %s", game_hash->game_id, game_hash->hash);
|
|
}
|
|
else if (game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) {
|
|
RC_CLIENT_LOG_INFO(client, "Switching to unknown media");
|
|
}
|
|
else if (game_hash->game_id == 0) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to unrecognized media: %s", game_hash->hash);
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Switching to known media for game %u: %s", game_hash->game_id, game_hash->hash);
|
|
}
|
|
|
|
client->game->public_.hash = game_hash->hash;
|
|
callback(RC_OK, NULL, client, callback_userdata);
|
|
}
|
|
|
|
static void rc_client_identify_changed_media_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
|
|
rc_client_t* client = load_state->client;
|
|
rc_api_resolve_hash_response_t resolve_hash_response;
|
|
|
|
int result = rc_api_process_resolve_hash_server_response(&resolve_hash_response, server_response);
|
|
const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &resolve_hash_response.response);
|
|
|
|
const int async_aborted = rc_client_end_async(client, &load_state->async_handle);
|
|
if (async_aborted) {
|
|
if (async_aborted != RC_CLIENT_ASYNC_DESTROYED) {
|
|
RC_CLIENT_LOG_VERBOSE(client, "Media change aborted");
|
|
/* if lookup succeeded, still capture the new hash */
|
|
if (result == RC_OK)
|
|
load_state->hash->game_id = resolve_hash_response.game_id;
|
|
}
|
|
}
|
|
else if (client->game != load_state->game) {
|
|
/* loaded game changed. return success regardless of result */
|
|
load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
|
|
}
|
|
else if (error_message) {
|
|
load_state->callback(result, error_message, client, load_state->callback_userdata);
|
|
}
|
|
else {
|
|
load_state->hash->game_id = resolve_hash_response.game_id;
|
|
|
|
if (resolve_hash_response.game_id == 0 && client->state.hardcore) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling hardcore for unidentified media: %s", load_state->hash->hash);
|
|
rc_client_set_hardcore_enabled(client, 0);
|
|
client->game->public_.hash = load_state->hash->hash; /* do still update the loaded hash */
|
|
load_state->callback(RC_HARDCORE_DISABLED, "Hardcore disabled. Unidentified media inserted.", client, load_state->callback_userdata);
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Identified game: %u (%s)", load_state->hash->game_id, load_state->hash->hash);
|
|
rc_client_change_media(client, load_state->hash, load_state->callback, load_state->callback_userdata);
|
|
}
|
|
}
|
|
|
|
free(load_state);
|
|
rc_api_destroy_resolve_hash_response(&resolve_hash_response);
|
|
}
|
|
|
|
rc_client_async_handle_t* rc_client_begin_change_media(rc_client_t* client, const char* file_path,
|
|
const uint8_t* data, size_t data_size, rc_client_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_client_game_hash_t* game_hash = NULL;
|
|
rc_client_media_hash_t* media_hash;
|
|
rc_client_game_info_t* game;
|
|
rc_client_pending_media_t* pending_media = NULL;
|
|
uint32_t path_djb2;
|
|
|
|
if (!client) {
|
|
callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
if (!data && !file_path) {
|
|
callback(RC_INVALID_STATE, "either data or file_path is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_change_media)
|
|
return client->state.external_client->begin_change_media(client, file_path, data, data_size, callback, callback_userdata);
|
|
#endif
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
if (client->state.load) {
|
|
game = client->state.load->game;
|
|
if (game->public_.console_id == 0) {
|
|
/* still waiting for game data */
|
|
pending_media = client->state.load->pending_media;
|
|
if (pending_media) {
|
|
if (pending_media->data)
|
|
free(pending_media->data);
|
|
free((void*)pending_media->file_path);
|
|
free(pending_media);
|
|
}
|
|
|
|
pending_media = (rc_client_pending_media_t*)calloc(1, sizeof(*pending_media));
|
|
if (!pending_media) {
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
pending_media->file_path = strdup(file_path);
|
|
pending_media->callback = callback;
|
|
pending_media->callback_userdata = callback_userdata;
|
|
if (data && data_size) {
|
|
pending_media->data_size = data_size;
|
|
pending_media->data = (uint8_t*)malloc(data_size);
|
|
if (!pending_media->data) {
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
memcpy(pending_media->data, data, data_size);
|
|
}
|
|
|
|
client->state.load->pending_media = pending_media;
|
|
}
|
|
}
|
|
else {
|
|
game = client->game;
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (!game) {
|
|
callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
/* still waiting for game data */
|
|
if (pending_media)
|
|
return NULL;
|
|
|
|
/* check to see if we've already hashed this file */
|
|
path_djb2 = rc_djb2(file_path);
|
|
rc_mutex_lock(&client->state.mutex);
|
|
for (media_hash = game->media_hash; media_hash; media_hash = media_hash->next) {
|
|
if (media_hash->path_djb2 == path_djb2) {
|
|
game_hash = media_hash->game_hash;
|
|
break;
|
|
}
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (!game_hash) {
|
|
char hash[33];
|
|
int result;
|
|
|
|
if (client->state.log_level >= RC_CLIENT_LOG_LEVEL_INFO) {
|
|
g_hash_client = client;
|
|
rc_hash_init_error_message_callback(rc_client_log_hash_message);
|
|
rc_hash_init_verbose_message_callback(rc_client_log_hash_message);
|
|
}
|
|
|
|
if (data != NULL)
|
|
result = rc_hash_generate_from_buffer(hash, game->public_.console_id, data, data_size);
|
|
else
|
|
result = rc_hash_generate_from_file(hash, game->public_.console_id, file_path);
|
|
|
|
g_hash_client = NULL;
|
|
|
|
if (!result) {
|
|
/* when changing discs, if the disc is not supported by the system, allow it. this is
|
|
* primarily for games that support user-provided audio CDs, but does allow using discs
|
|
* from other systems for games that leverage user-provided discs. */
|
|
strcpy_s(hash, sizeof(hash), "[NO HASH]");
|
|
}
|
|
|
|
game_hash = rc_client_find_game_hash(client, hash);
|
|
|
|
media_hash = (rc_client_media_hash_t*)rc_buffer_alloc(&game->buffer, sizeof(*media_hash));
|
|
media_hash->game_hash = game_hash;
|
|
media_hash->path_djb2 = path_djb2;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
media_hash->next = game->media_hash;
|
|
game->media_hash = media_hash;
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (!result) {
|
|
rc_client_change_media(client, game_hash, callback, callback_userdata);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
if (game_hash->game_id != RC_CLIENT_UNKNOWN_GAME_ID) {
|
|
rc_client_change_media(client, game_hash, callback, callback_userdata);
|
|
return NULL;
|
|
}
|
|
else {
|
|
/* call the server to make sure the hash is valid for the loaded game */
|
|
rc_client_load_state_t* callback_data;
|
|
rc_client_async_handle_t* async_handle;
|
|
rc_api_resolve_hash_request_t resolve_hash_request;
|
|
rc_api_request_t request;
|
|
int result;
|
|
|
|
memset(&resolve_hash_request, 0, sizeof(resolve_hash_request));
|
|
resolve_hash_request.game_hash = game_hash->hash;
|
|
|
|
result = rc_api_init_resolve_hash_request(&request, &resolve_hash_request);
|
|
if (result != RC_OK) {
|
|
callback(result, rc_error_str(result), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
callback_data = (rc_client_load_state_t*)calloc(1, sizeof(rc_client_load_state_t));
|
|
if (!callback_data) {
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
callback_data->callback = callback;
|
|
callback_data->callback_userdata = callback_userdata;
|
|
callback_data->client = client;
|
|
callback_data->hash = game_hash;
|
|
callback_data->game = game;
|
|
|
|
async_handle = &callback_data->async_handle;
|
|
rc_client_begin_async(client, async_handle);
|
|
client->callbacks.server_call(&request, rc_client_identify_changed_media_callback, callback_data, client);
|
|
|
|
rc_api_destroy_request(&request);
|
|
|
|
/* if handle is no longer valid, the async operation completed synchronously */
|
|
return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL;
|
|
}
|
|
}
|
|
|
|
const rc_client_game_t* rc_client_get_game_info(const rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return NULL;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_game_info)
|
|
return client->state.external_client->get_game_info();
|
|
#endif
|
|
|
|
return client->game ? &client->game->public_ : NULL;
|
|
}
|
|
|
|
int rc_client_game_get_image_url(const rc_client_game_t* game, char buffer[], size_t buffer_size)
|
|
{
|
|
if (!game)
|
|
return RC_INVALID_STATE;
|
|
|
|
return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_GAME, game->badge_name);
|
|
}
|
|
|
|
/* ===== Subsets ===== */
|
|
|
|
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)
|
|
{
|
|
char buffer[32];
|
|
rc_client_load_state_t* load_state;
|
|
|
|
if (!client) {
|
|
callback(RC_INVALID_STATE, "client is required", client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_load_subset)
|
|
return client->state.external_client->begin_load_subset(client, subset_id, callback, callback_userdata);
|
|
#endif
|
|
|
|
if (!client->game) {
|
|
callback(RC_NO_GAME_LOADED, rc_error_str(RC_NO_GAME_LOADED), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)subset_id);
|
|
|
|
load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
|
|
if (!load_state) {
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
load_state->client = client;
|
|
load_state->callback = callback;
|
|
load_state->callback_userdata = callback_userdata;
|
|
load_state->game = client->game;
|
|
load_state->hash = rc_client_find_game_hash(client, buffer);
|
|
load_state->hash->game_id = subset_id;
|
|
client->state.load = load_state;
|
|
|
|
rc_client_begin_fetch_game_data(load_state);
|
|
|
|
return (client->state.load == load_state) ? &load_state->async_handle : NULL;
|
|
}
|
|
|
|
const rc_client_subset_t* rc_client_get_subset_info(rc_client_t* client, uint32_t subset_id)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
|
|
if (!client)
|
|
return NULL;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_subset_info)
|
|
return client->state.external_client->get_subset_info(subset_id);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return NULL;
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (subset->public_.id == subset_id)
|
|
return &subset->public_;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* ===== Achievements ===== */
|
|
|
|
static void rc_client_update_achievement_display_information(rc_client_t* client, rc_client_achievement_info_t* achievement, time_t recent_unlock_time)
|
|
{
|
|
uint8_t new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNKNOWN;
|
|
uint32_t new_measured_value = 0;
|
|
|
|
if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED)
|
|
return;
|
|
|
|
achievement->public_.measured_progress[0] = '\0';
|
|
|
|
if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) {
|
|
/* achievement unlocked */
|
|
if (achievement->public_.unlock_time >= recent_unlock_time) {
|
|
new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED;
|
|
} else {
|
|
new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED;
|
|
|
|
if (client->state.disconnect && rc_client_is_award_achievement_pending(client, achievement->public_.id))
|
|
new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED;
|
|
}
|
|
}
|
|
else {
|
|
/* active achievement */
|
|
new_bucket = (achievement->public_.category == RC_CLIENT_ACHIEVEMENT_CATEGORY_UNOFFICIAL) ?
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL : RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED;
|
|
|
|
if (achievement->trigger) {
|
|
if (achievement->trigger->measured_target) {
|
|
if (achievement->trigger->measured_value == RC_MEASURED_UNKNOWN) {
|
|
/* value hasn't been initialized yet, leave progress string empty */
|
|
}
|
|
else if (achievement->trigger->measured_value == 0) {
|
|
/* value is 0, leave progress string empty. update progress to 0.0 */
|
|
achievement->public_.measured_percent = 0.0;
|
|
}
|
|
else {
|
|
/* clamp measured value at target (can't get more than 100%) */
|
|
new_measured_value = (achievement->trigger->measured_value > achievement->trigger->measured_target) ?
|
|
achievement->trigger->measured_target : achievement->trigger->measured_value;
|
|
|
|
achievement->public_.measured_percent = ((float)new_measured_value * 100) / (float)achievement->trigger->measured_target;
|
|
|
|
if (!achievement->trigger->measured_as_percent) {
|
|
snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress),
|
|
"%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target);
|
|
}
|
|
else if (achievement->public_.measured_percent >= 1.0) {
|
|
snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress),
|
|
"%lu%%", (unsigned long)achievement->public_.measured_percent);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (achievement->trigger->state == RC_TRIGGER_STATE_PRIMED)
|
|
new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE;
|
|
else if (achievement->public_.measured_percent >= 80.0)
|
|
new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE;
|
|
}
|
|
}
|
|
|
|
achievement->public_.bucket = new_bucket;
|
|
}
|
|
|
|
static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type)
|
|
{
|
|
switch (bucket_type) {
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: return "Locked";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: return "Unlocked";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: return "Unsupported";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: return "Unofficial";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There";
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: return "Unlocks Not Synced to Server";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
const char** ptr;
|
|
const char* label;
|
|
char* new_label;
|
|
size_t new_label_len;
|
|
|
|
switch (bucket_type) {
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED: ptr = &subset->locked_label; break;
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED: ptr = &subset->unlocked_label; break;
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break;
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL: ptr = &subset->unofficial_label; break;
|
|
default: return rc_client_get_achievement_bucket_label(bucket_type);
|
|
}
|
|
|
|
if (*ptr)
|
|
return *ptr;
|
|
|
|
label = rc_client_get_achievement_bucket_label(bucket_type);
|
|
new_label_len = strlen(subset->public_.title) + strlen(label) + 4;
|
|
new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len);
|
|
snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label);
|
|
|
|
*ptr = new_label;
|
|
return new_label;
|
|
}
|
|
|
|
static int rc_client_compare_achievement_unlock_times(const void* a, const void* b)
|
|
{
|
|
const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a;
|
|
const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b;
|
|
if (unlock_b->unlock_time == unlock_a->unlock_time)
|
|
return 0;
|
|
return (unlock_b->unlock_time < unlock_a->unlock_time) ? -1 : 1;
|
|
}
|
|
|
|
static int rc_client_compare_achievement_progress(const void* a, const void* b)
|
|
{
|
|
const rc_client_achievement_t* unlock_a = *(const rc_client_achievement_t**)a;
|
|
const rc_client_achievement_t* unlock_b = *(const rc_client_achievement_t**)b;
|
|
if (unlock_b->measured_percent == unlock_a->measured_percent) {
|
|
if (unlock_a->id == unlock_b->id)
|
|
return 0;
|
|
return (unlock_a->id < unlock_b->id) ? -1 : 1;
|
|
}
|
|
return (unlock_b->measured_percent < unlock_a->measured_percent) ? -1 : 1;
|
|
}
|
|
|
|
static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping)
|
|
{
|
|
if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) {
|
|
switch (bucket) {
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED:
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED:
|
|
return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED;
|
|
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE:
|
|
case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE:
|
|
return RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED;
|
|
|
|
default:
|
|
return bucket;
|
|
}
|
|
}
|
|
|
|
return bucket;
|
|
}
|
|
|
|
rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping)
|
|
{
|
|
rc_client_achievement_info_t* achievement;
|
|
rc_client_achievement_info_t* stop;
|
|
rc_client_achievement_t** bucket_achievements;
|
|
rc_client_achievement_t** achievement_ptr;
|
|
rc_client_achievement_bucket_t* bucket_ptr;
|
|
rc_client_achievement_list_info_t* list;
|
|
rc_client_subset_info_t* subset;
|
|
const uint32_t list_size = RC_ALIGN(sizeof(*list));
|
|
uint32_t bucket_counts[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS];
|
|
uint32_t num_buckets;
|
|
uint32_t num_achievements;
|
|
size_t buckets_size;
|
|
uint8_t bucket_type;
|
|
uint32_t num_subsets = 0;
|
|
uint32_t i, j;
|
|
const uint8_t shared_bucket_order[] = {
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE,
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED,
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE,
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED,
|
|
};
|
|
const uint8_t subset_bucket_order[] = {
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED,
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_UNOFFICIAL,
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED,
|
|
RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED
|
|
};
|
|
const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS;
|
|
|
|
if (!client)
|
|
return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t));
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->create_achievement_list)
|
|
return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return (rc_client_achievement_list_t*)calloc(1, sizeof(rc_client_achievement_list_info_t));
|
|
|
|
memset(&bucket_counts, 0, sizeof(bucket_counts));
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
subset = client->game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
num_subsets++;
|
|
achievement = subset->achievements;
|
|
stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.category & category) {
|
|
rc_client_update_achievement_display_information(client, achievement, recent_unlock_time);
|
|
bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
num_buckets = 0;
|
|
num_achievements = 0;
|
|
for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) {
|
|
if (bucket_counts[i]) {
|
|
int needs_split = 0;
|
|
|
|
num_achievements += bucket_counts[i];
|
|
|
|
if (num_subsets > 1) {
|
|
for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) {
|
|
if (subset_bucket_order[j] == i) {
|
|
needs_split = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!needs_split) {
|
|
++num_buckets;
|
|
continue;
|
|
}
|
|
|
|
subset = client->game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
achievement = subset->achievements;
|
|
stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.category & category) {
|
|
if (rc_client_map_bucket(achievement->public_.bucket, grouping) == i) {
|
|
++num_buckets;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_achievement_bucket_t));
|
|
|
|
list = (rc_client_achievement_list_info_t*)malloc(list_size + buckets_size + num_achievements * sizeof(rc_client_achievement_t*));
|
|
bucket_ptr = list->public_.buckets = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size);
|
|
achievement_ptr = (rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size);
|
|
|
|
if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) {
|
|
for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) {
|
|
bucket_type = shared_bucket_order[i];
|
|
if (!bucket_counts[bucket_type])
|
|
continue;
|
|
|
|
bucket_achievements = achievement_ptr;
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
achievement = subset->achievements;
|
|
stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.category & category &&
|
|
rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) {
|
|
*achievement_ptr++ = &achievement->public_;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (achievement_ptr > bucket_achievements) {
|
|
bucket_ptr->achievements = bucket_achievements;
|
|
bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements);
|
|
bucket_ptr->subset_id = 0;
|
|
bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type);
|
|
bucket_ptr->bucket_type = bucket_type;
|
|
|
|
if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED)
|
|
qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_unlock_times);
|
|
else if (bucket_type == RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE)
|
|
qsort(bucket_ptr->achievements, bucket_ptr->num_achievements, sizeof(rc_client_achievement_t*), rc_client_compare_achievement_progress);
|
|
|
|
++bucket_ptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) {
|
|
bucket_type = subset_bucket_order[i];
|
|
if (!bucket_counts[bucket_type])
|
|
continue;
|
|
|
|
bucket_achievements = achievement_ptr;
|
|
|
|
achievement = subset->achievements;
|
|
stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.category & category &&
|
|
rc_client_map_bucket(achievement->public_.bucket, grouping) == bucket_type) {
|
|
*achievement_ptr++ = &achievement->public_;
|
|
}
|
|
}
|
|
|
|
if (achievement_ptr > bucket_achievements) {
|
|
bucket_ptr->achievements = bucket_achievements;
|
|
bucket_ptr->num_achievements = (uint32_t)(achievement_ptr - bucket_achievements);
|
|
bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0;
|
|
bucket_ptr->bucket_type = bucket_type;
|
|
|
|
if (num_subsets > 1)
|
|
bucket_ptr->label = rc_client_get_subset_achievement_bucket_label(bucket_type, client->game, subset);
|
|
else
|
|
bucket_ptr->label = rc_client_get_achievement_bucket_label(bucket_type);
|
|
|
|
++bucket_ptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
list->destroy_func = NULL;
|
|
list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets);
|
|
return &list->public_;
|
|
}
|
|
|
|
void rc_client_destroy_achievement_list(rc_client_achievement_list_t* list)
|
|
{
|
|
rc_client_achievement_list_info_t* info = (rc_client_achievement_list_info_t*)list;
|
|
if (info->destroy_func)
|
|
info->destroy_func(info);
|
|
else
|
|
free(list);
|
|
}
|
|
|
|
int rc_client_has_achievements(rc_client_t* client)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
int result;
|
|
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->has_achievements)
|
|
return client->state.external_client->has_achievements();
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return 0;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
subset = client->game->subsets;
|
|
result = 0;
|
|
for (; subset; subset = subset->next)
|
|
{
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
if (subset->public_.num_achievements > 0) {
|
|
result = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return result;
|
|
}
|
|
|
|
static const rc_client_achievement_t* rc_client_subset_get_achievement_info(
|
|
rc_client_t* client, rc_client_subset_info_t* subset, uint32_t id)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->public_.id == id) {
|
|
const time_t recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS;
|
|
rc_mutex_lock((rc_mutex_t*)(&client->state.mutex));
|
|
rc_client_update_achievement_display_information(client, achievement, recent_unlock_time);
|
|
rc_mutex_unlock((rc_mutex_t*)(&client->state.mutex));
|
|
return &achievement->public_;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
const rc_client_achievement_t* rc_client_get_achievement_info(rc_client_t* client, uint32_t id)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
|
|
if (!client)
|
|
return NULL;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_achievement_info)
|
|
return client->state.external_client->get_achievement_info(id);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return NULL;
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
const rc_client_achievement_t* achievement = rc_client_subset_get_achievement_info(client, subset, id);
|
|
if (achievement != NULL)
|
|
return achievement;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int rc_client_achievement_get_image_url(const rc_client_achievement_t* achievement, int state, char buffer[], size_t buffer_size)
|
|
{
|
|
const int image_type = (state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) ?
|
|
RC_IMAGE_TYPE_ACHIEVEMENT : RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED;
|
|
|
|
if (!achievement || !achievement->badge_name[0])
|
|
return rc_client_get_image_url(buffer, buffer_size, image_type, "00000");
|
|
|
|
return rc_client_get_image_url(buffer, buffer_size, image_type, achievement->badge_name);
|
|
}
|
|
|
|
typedef struct rc_client_award_achievement_callback_data_t
|
|
{
|
|
uint32_t id;
|
|
uint32_t retry_count;
|
|
uint8_t hardcore;
|
|
const char* game_hash;
|
|
time_t unlock_time;
|
|
rc_client_t* client;
|
|
rc_client_scheduled_callback_data_t* scheduled_callback_data;
|
|
} rc_client_award_achievement_callback_data_t;
|
|
|
|
static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id)
|
|
{
|
|
/* assume lock already held */
|
|
rc_client_scheduled_callback_data_t* scheduled_callback = client->state.scheduled_callbacks;
|
|
for (; scheduled_callback; scheduled_callback = scheduled_callback->next)
|
|
{
|
|
if (scheduled_callback->callback == rc_client_award_achievement_retry)
|
|
{
|
|
rc_client_award_achievement_callback_data_t* ach_data =
|
|
(rc_client_award_achievement_callback_data_t*)scheduled_callback->data;
|
|
if (ach_data->id == achievement_id)
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data);
|
|
|
|
static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
|
|
{
|
|
rc_client_award_achievement_callback_data_t* ach_data =
|
|
(rc_client_award_achievement_callback_data_t*)callback_data->data;
|
|
|
|
(void)client;
|
|
(void)now;
|
|
|
|
rc_client_award_achievement_server_call(ach_data);
|
|
}
|
|
|
|
static void rc_client_award_achievement_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_award_achievement_callback_data_t* ach_data =
|
|
(rc_client_award_achievement_callback_data_t*)callback_data;
|
|
rc_api_award_achievement_response_t award_achievement_response;
|
|
|
|
int result = rc_api_process_award_achievement_server_response(&award_achievement_response, server_response);
|
|
const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &award_achievement_response.response);
|
|
|
|
if (error_message) {
|
|
if (award_achievement_response.response.error_message && !rc_client_should_retry(server_response)) {
|
|
/* actual error from server */
|
|
RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s", ach_data->id, error_message);
|
|
rc_client_raise_server_error_event(ach_data->client, "award_achievement", ach_data->id, result, award_achievement_response.response.error_message);
|
|
}
|
|
else if (ach_data->retry_count++ == 0) {
|
|
/* first retry is immediate */
|
|
RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying immediately", ach_data->id, error_message);
|
|
rc_client_award_achievement_server_call(ach_data);
|
|
return;
|
|
}
|
|
else {
|
|
/* double wait time between each attempt until we hit a maximum delay of two minutes */
|
|
/* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/
|
|
const uint32_t delay = (ach_data->retry_count > 8) ? 120 : (1 << (ach_data->retry_count - 2));
|
|
RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error awarding achievement %u: %s, retrying in %u seconds", ach_data->id, error_message, delay);
|
|
|
|
if (!ach_data->scheduled_callback_data) {
|
|
ach_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*ach_data->scheduled_callback_data));
|
|
if (!ach_data->scheduled_callback_data) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Failed to allocate scheduled callback data for reattempt to unlock achievement %u", ach_data->id);
|
|
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));
|
|
return;
|
|
}
|
|
ach_data->scheduled_callback_data->callback = rc_client_award_achievement_retry;
|
|
ach_data->scheduled_callback_data->data = ach_data;
|
|
ach_data->scheduled_callback_data->related_id = ach_data->id;
|
|
}
|
|
|
|
ach_data->scheduled_callback_data->when =
|
|
ach_data->client->callbacks.get_time_millisecs(ach_data->client) + delay * 1000;
|
|
|
|
rc_client_schedule_callback(ach_data->client, ach_data->scheduled_callback_data);
|
|
|
|
rc_client_update_disconnect_state(ach_data->client);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
ach_data->client->user.score = award_achievement_response.new_player_score;
|
|
ach_data->client->user.score_softcore = award_achievement_response.new_player_score_softcore;
|
|
|
|
if (award_achievement_response.awarded_achievement_id != ach_data->id) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Awarded achievement %u instead of %u", award_achievement_response.awarded_achievement_id, error_message);
|
|
}
|
|
else {
|
|
if (award_achievement_response.response.error_message) {
|
|
/* previously unlocked achievements are returned as a success with an error message */
|
|
RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u: %s", ach_data->id, award_achievement_response.response.error_message);
|
|
}
|
|
else if (ach_data->retry_count) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded after %u attempts, new score: %u",
|
|
ach_data->id, ach_data->retry_count + 1,
|
|
ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore);
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Achievement %u awarded, new score: %u",
|
|
ach_data->id,
|
|
ach_data->hardcore ? award_achievement_response.new_player_score : award_achievement_response.new_player_score_softcore);
|
|
}
|
|
|
|
if (award_achievement_response.achievements_remaining == 0) {
|
|
rc_client_subset_info_t* subset;
|
|
for (subset = ach_data->client->game->subsets; subset; subset = subset->next) {
|
|
if (subset->mastery == RC_CLIENT_MASTERY_STATE_NONE &&
|
|
rc_client_subset_get_achievement_info(ach_data->client, subset, ach_data->id)) {
|
|
if (subset->public_.id == ach_data->client->game->public_.id) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Game %u %s", ach_data->client->game->public_.id,
|
|
ach_data->client->state.hardcore ? "mastered" : "completed");
|
|
subset->mastery = RC_CLIENT_MASTERY_STATE_PENDING;
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(ach_data->client, "Subset %u %s", ach_data->client->game->public_.id,
|
|
ach_data->client->state.hardcore ? "mastered" : "completed");
|
|
|
|
/* TODO: subset mastery notification */
|
|
subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ach_data->retry_count)
|
|
rc_client_update_disconnect_state(ach_data->client);
|
|
|
|
if (ach_data->scheduled_callback_data)
|
|
free(ach_data->scheduled_callback_data);
|
|
free(ach_data);
|
|
}
|
|
|
|
static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data)
|
|
{
|
|
rc_api_award_achievement_request_t api_params;
|
|
rc_api_request_t request;
|
|
int result;
|
|
|
|
memset(&api_params, 0, sizeof(api_params));
|
|
api_params.username = ach_data->client->user.username;
|
|
api_params.api_token = ach_data->client->user.token;
|
|
api_params.achievement_id = ach_data->id;
|
|
api_params.hardcore = ach_data->hardcore;
|
|
api_params.game_hash = ach_data->game_hash;
|
|
|
|
result = rc_api_init_award_achievement_request(&request, &api_params);
|
|
if (result != RC_OK) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(ach_data->client, "Error constructing unlock request for achievement %u: %s", ach_data->id, rc_error_str(result));
|
|
free(ach_data);
|
|
return;
|
|
}
|
|
|
|
ach_data->client->callbacks.server_call(&request, rc_client_award_achievement_callback, ach_data, ach_data->client);
|
|
|
|
rc_api_destroy_request(&request);
|
|
}
|
|
|
|
static void rc_client_award_achievement(rc_client_t* client, rc_client_achievement_info_t* achievement)
|
|
{
|
|
rc_client_award_achievement_callback_data_t* callback_data;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
if (client->state.hardcore) {
|
|
achievement->public_.unlock_time = achievement->unlock_time_hardcore = time(NULL);
|
|
if (achievement->unlock_time_softcore == 0)
|
|
achievement->unlock_time_softcore = achievement->unlock_time_hardcore;
|
|
|
|
/* adjust score now - will get accurate score back from server */
|
|
client->user.score += achievement->public_.points;
|
|
}
|
|
else {
|
|
achievement->public_.unlock_time = achievement->unlock_time_softcore = time(NULL);
|
|
|
|
/* adjust score now - will get accurate score back from server */
|
|
client->user.score_softcore += achievement->public_.points;
|
|
}
|
|
|
|
achievement->public_.state = RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED;
|
|
achievement->public_.unlocked |= (client->state.hardcore) ?
|
|
RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH : RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE;
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (client->callbacks.can_submit_achievement_unlock &&
|
|
!client->callbacks.can_submit_achievement_unlock(achievement->public_.id, client)) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Achievement %u unlock blocked by client", achievement->public_.id);
|
|
return;
|
|
}
|
|
|
|
/* can't unlock unofficial achievements on the server */
|
|
if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title);
|
|
return;
|
|
}
|
|
|
|
/* don't actually unlock achievements when spectating */
|
|
if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated achievement %u: %s", achievement->public_.id, achievement->public_.title);
|
|
return;
|
|
}
|
|
|
|
callback_data = (rc_client_award_achievement_callback_data_t*)calloc(1, sizeof(*callback_data));
|
|
if (!callback_data) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for unlocking achievement %u", achievement->public_.id);
|
|
rc_client_raise_server_error_event(client, "award_achievement", achievement->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
|
|
return;
|
|
}
|
|
callback_data->client = client;
|
|
callback_data->id = achievement->public_.id;
|
|
callback_data->hardcore = client->state.hardcore;
|
|
callback_data->game_hash = client->game->public_.hash;
|
|
callback_data->unlock_time = achievement->public_.unlock_time;
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Awarding achievement %u: %s", achievement->public_.id, achievement->public_.title);
|
|
rc_client_award_achievement_server_call(callback_data);
|
|
}
|
|
|
|
static void rc_client_subset_reset_achievements(rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
rc_trigger_t* trigger = achievement->trigger;
|
|
if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
|
|
continue;
|
|
|
|
if (trigger->state == RC_TRIGGER_STATE_PRIMED) {
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
|
|
}
|
|
|
|
rc_reset_trigger(trigger);
|
|
}
|
|
}
|
|
|
|
static void rc_client_reset_achievements(rc_client_t* client)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
for (subset = client->game->subsets; subset; subset = subset->next)
|
|
rc_client_subset_reset_achievements(subset);
|
|
}
|
|
|
|
/* ===== Leaderboards ===== */
|
|
|
|
static rc_client_leaderboard_info_t* rc_client_subset_get_leaderboard_info(const rc_client_subset_info_t* subset, uint32_t id)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
|
|
rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
|
|
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->public_.id == id)
|
|
return leaderboard;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* client, uint32_t id)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
|
|
if (!client)
|
|
return NULL;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_leaderboard_info)
|
|
return client->state.external_client->get_leaderboard_info(id);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return NULL;
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
const rc_client_leaderboard_info_t* leaderboard = rc_client_subset_get_leaderboard_info(subset, id);
|
|
if (leaderboard != NULL)
|
|
return &leaderboard->public_;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static const char* rc_client_get_leaderboard_bucket_label(uint8_t bucket_type)
|
|
{
|
|
switch (bucket_type) {
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: return "Inactive";
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE: return "Active";
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: return "Unsupported";
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_ALL: return "All";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
const char** ptr;
|
|
const char* label;
|
|
char* new_label;
|
|
size_t new_label_len;
|
|
|
|
switch (bucket_type) {
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE: ptr = &subset->inactive_label; break;
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED: ptr = &subset->unsupported_label; break;
|
|
case RC_CLIENT_LEADERBOARD_BUCKET_ALL: ptr = &subset->all_label; break;
|
|
default: return rc_client_get_achievement_bucket_label(bucket_type);
|
|
}
|
|
|
|
if (*ptr)
|
|
return *ptr;
|
|
|
|
label = rc_client_get_leaderboard_bucket_label(bucket_type);
|
|
new_label_len = strlen(subset->public_.title) + strlen(label) + 4;
|
|
new_label = (char*)rc_buffer_alloc(&game->buffer, new_label_len);
|
|
snprintf(new_label, new_label_len, "%s - %s", subset->public_.title, label);
|
|
|
|
*ptr = new_label;
|
|
return new_label;
|
|
}
|
|
|
|
static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info_t* leaderboard, int grouping)
|
|
{
|
|
switch (leaderboard->public_.state) {
|
|
case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
|
|
return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ?
|
|
RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE;
|
|
|
|
case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
|
|
return RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED;
|
|
|
|
default:
|
|
return (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) ?
|
|
RC_CLIENT_LEADERBOARD_BUCKET_ALL : RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE;
|
|
}
|
|
}
|
|
|
|
rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* stop;
|
|
rc_client_leaderboard_t** bucket_leaderboards;
|
|
rc_client_leaderboard_t** leaderboard_ptr;
|
|
rc_client_leaderboard_bucket_t* bucket_ptr;
|
|
rc_client_leaderboard_list_info_t* list;
|
|
rc_client_subset_info_t* subset;
|
|
const uint32_t list_size = RC_ALIGN(sizeof(*list));
|
|
uint32_t bucket_counts[8];
|
|
uint32_t num_buckets;
|
|
uint32_t num_leaderboards;
|
|
size_t buckets_size;
|
|
uint8_t bucket_type;
|
|
uint32_t num_subsets = 0;
|
|
uint32_t i, j;
|
|
const uint8_t shared_bucket_order[] = {
|
|
RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE
|
|
};
|
|
const uint8_t subset_bucket_order[] = {
|
|
RC_CLIENT_LEADERBOARD_BUCKET_ALL,
|
|
RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE,
|
|
RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED
|
|
};
|
|
|
|
if (!client)
|
|
return calloc(1, sizeof(rc_client_leaderboard_list_t));
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->create_leaderboard_list)
|
|
return (rc_client_leaderboard_list_t*)client->state.external_client->create_leaderboard_list(grouping);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return calloc(1, sizeof(rc_client_leaderboard_list_t));
|
|
|
|
memset(&bucket_counts, 0, sizeof(bucket_counts));
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
subset = client->game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
num_subsets++;
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->hidden)
|
|
continue;
|
|
|
|
leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping);
|
|
bucket_counts[leaderboard->bucket]++;
|
|
}
|
|
}
|
|
|
|
num_buckets = 0;
|
|
num_leaderboards = 0;
|
|
for (i = 0; i < sizeof(bucket_counts) / sizeof(bucket_counts[0]); ++i) {
|
|
if (bucket_counts[i]) {
|
|
int needs_split = 0;
|
|
|
|
num_leaderboards += bucket_counts[i];
|
|
|
|
if (num_subsets > 1) {
|
|
for (j = 0; j < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++j) {
|
|
if (subset_bucket_order[j] == i) {
|
|
needs_split = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!needs_split) {
|
|
++num_buckets;
|
|
continue;
|
|
}
|
|
|
|
subset = client->game->subsets;
|
|
for (; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->bucket == i) {
|
|
++num_buckets;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
buckets_size = RC_ALIGN(num_buckets * sizeof(rc_client_leaderboard_bucket_t));
|
|
|
|
list = (rc_client_leaderboard_list_info_t*)malloc(list_size + buckets_size + num_leaderboards * sizeof(rc_client_leaderboard_t*));
|
|
bucket_ptr = list->public_.buckets = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size);
|
|
leaderboard_ptr = (rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size);
|
|
|
|
if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) {
|
|
for (i = 0; i < sizeof(shared_bucket_order) / sizeof(shared_bucket_order[0]); ++i) {
|
|
bucket_type = shared_bucket_order[i];
|
|
if (!bucket_counts[bucket_type])
|
|
continue;
|
|
|
|
bucket_leaderboards = leaderboard_ptr;
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->bucket == bucket_type && !leaderboard->hidden)
|
|
*leaderboard_ptr++ = &leaderboard->public_;
|
|
}
|
|
}
|
|
|
|
if (leaderboard_ptr > bucket_leaderboards) {
|
|
bucket_ptr->leaderboards = bucket_leaderboards;
|
|
bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards);
|
|
bucket_ptr->subset_id = 0;
|
|
bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type);
|
|
bucket_ptr->bucket_type = bucket_type;
|
|
++bucket_ptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) {
|
|
bucket_type = subset_bucket_order[i];
|
|
if (!bucket_counts[bucket_type])
|
|
continue;
|
|
|
|
bucket_leaderboards = leaderboard_ptr;
|
|
|
|
leaderboard = subset->leaderboards;
|
|
stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
if (leaderboard->bucket == bucket_type && !leaderboard->hidden)
|
|
*leaderboard_ptr++ = &leaderboard->public_;
|
|
}
|
|
|
|
if (leaderboard_ptr > bucket_leaderboards) {
|
|
bucket_ptr->leaderboards = bucket_leaderboards;
|
|
bucket_ptr->num_leaderboards = (uint32_t)(leaderboard_ptr - bucket_leaderboards);
|
|
bucket_ptr->subset_id = (num_subsets > 1) ? subset->public_.id : 0;
|
|
bucket_ptr->bucket_type = bucket_type;
|
|
|
|
if (num_subsets > 1)
|
|
bucket_ptr->label = rc_client_get_subset_leaderboard_bucket_label(bucket_type, client->game, subset);
|
|
else
|
|
bucket_ptr->label = rc_client_get_leaderboard_bucket_label(bucket_type);
|
|
|
|
++bucket_ptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
list->destroy_func = NULL;
|
|
list->public_.num_buckets = (uint32_t)(bucket_ptr - list->public_.buckets);
|
|
return &list->public_;
|
|
}
|
|
|
|
void rc_client_destroy_leaderboard_list(rc_client_leaderboard_list_t* list)
|
|
{
|
|
rc_client_leaderboard_list_info_t* info = (rc_client_leaderboard_list_info_t*)list;
|
|
if (info->destroy_func)
|
|
info->destroy_func(info);
|
|
else
|
|
free(list);
|
|
}
|
|
|
|
int rc_client_has_leaderboards(rc_client_t* client)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
int result;
|
|
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->has_leaderboards)
|
|
return client->state.external_client->has_leaderboards();
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return 0;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
subset = client->game->subsets;
|
|
result = 0;
|
|
for (; subset; subset = subset->next)
|
|
{
|
|
if (!subset->active)
|
|
continue;
|
|
|
|
if (subset->public_.num_leaderboards > 0) {
|
|
result = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return result;
|
|
}
|
|
|
|
void rc_client_allocate_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
|
|
{
|
|
rc_client_leaderboard_tracker_info_t* tracker;
|
|
rc_client_leaderboard_tracker_info_t* available_tracker = NULL;
|
|
|
|
for (tracker = game->leaderboard_trackers; tracker; tracker = tracker->next) {
|
|
if (tracker->reference_count == 0) {
|
|
if (available_tracker == NULL && tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE)
|
|
available_tracker = tracker;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (tracker->value_djb2 != leaderboard->value_djb2 || tracker->format != leaderboard->format)
|
|
continue;
|
|
|
|
if (tracker->raw_value != leaderboard->value) {
|
|
/* if the value comes from tracking hits, we can't assume the trackers started in the
|
|
* same frame, so we can't share the tracker */
|
|
if (tracker->value_from_hits)
|
|
continue;
|
|
|
|
/* value has changed. prepare an update event */
|
|
tracker->raw_value = leaderboard->value;
|
|
tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE;
|
|
game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
|
|
}
|
|
|
|
/* attach to the existing tracker */
|
|
++tracker->reference_count;
|
|
tracker->pending_events &= ~RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE;
|
|
leaderboard->tracker = tracker;
|
|
leaderboard->public_.tracker_value = tracker->public_.display;
|
|
return;
|
|
}
|
|
|
|
if (!available_tracker) {
|
|
rc_client_leaderboard_tracker_info_t** next = &game->leaderboard_trackers;
|
|
|
|
available_tracker = (rc_client_leaderboard_tracker_info_t*)rc_buffer_alloc(&game->buffer, sizeof(*available_tracker));
|
|
memset(available_tracker, 0, sizeof(*available_tracker));
|
|
available_tracker->public_.id = 1;
|
|
|
|
for (tracker = *next; tracker; next = &tracker->next, tracker = *next)
|
|
available_tracker->public_.id++;
|
|
|
|
*next = available_tracker;
|
|
}
|
|
|
|
/* update the claimed tracker */
|
|
available_tracker->reference_count = 1;
|
|
available_tracker->value_djb2 = leaderboard->value_djb2;
|
|
available_tracker->format = leaderboard->format;
|
|
available_tracker->raw_value = leaderboard->value;
|
|
available_tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW;
|
|
available_tracker->value_from_hits = rc_value_from_hits(&leaderboard->lboard->value);
|
|
leaderboard->tracker = available_tracker;
|
|
leaderboard->public_.tracker_value = available_tracker->public_.display;
|
|
game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
|
|
}
|
|
|
|
void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
|
|
{
|
|
rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker;
|
|
leaderboard->tracker = NULL;
|
|
|
|
if (tracker && --tracker->reference_count == 0) {
|
|
tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE;
|
|
game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
|
|
}
|
|
}
|
|
|
|
static void rc_client_update_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard)
|
|
{
|
|
rc_client_leaderboard_tracker_info_t* tracker = leaderboard->tracker;
|
|
if (tracker && tracker->raw_value != leaderboard->value) {
|
|
tracker->raw_value = leaderboard->value;
|
|
tracker->pending_events |= RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE;
|
|
game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER;
|
|
}
|
|
}
|
|
|
|
typedef struct rc_client_submit_leaderboard_entry_callback_data_t
|
|
{
|
|
uint32_t id;
|
|
int32_t score;
|
|
uint32_t retry_count;
|
|
const char* game_hash;
|
|
time_t submit_time;
|
|
rc_client_t* client;
|
|
rc_client_scheduled_callback_data_t* scheduled_callback_data;
|
|
} rc_client_submit_leaderboard_entry_callback_data_t;
|
|
|
|
static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data);
|
|
|
|
static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
|
|
{
|
|
rc_client_submit_leaderboard_entry_callback_data_t* lboard_data =
|
|
(rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data;
|
|
|
|
(void)client;
|
|
(void)now;
|
|
|
|
rc_client_submit_leaderboard_entry_server_call(lboard_data);
|
|
}
|
|
|
|
static void rc_client_raise_scoreboard_event(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data,
|
|
const rc_api_submit_lboard_entry_response_t* response)
|
|
{
|
|
rc_client_leaderboard_scoreboard_t sboard;
|
|
rc_client_event_t client_event;
|
|
rc_client_subset_info_t* subset;
|
|
rc_client_t* client = lboard_data->client;
|
|
rc_client_leaderboard_info_t* leaderboard = NULL;
|
|
|
|
if (!client || !client->game)
|
|
return;
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
leaderboard = rc_client_subset_get_leaderboard_info(subset, lboard_data->id);
|
|
if (leaderboard != NULL)
|
|
break;
|
|
}
|
|
if (leaderboard == NULL) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(client, "Trying to raise scoreboard for unknown leaderboard %u", lboard_data->id);
|
|
return;
|
|
}
|
|
|
|
memset(&sboard, 0, sizeof(sboard));
|
|
sboard.leaderboard_id = lboard_data->id;
|
|
rc_format_value(sboard.submitted_score, sizeof(sboard.submitted_score), response->submitted_score, leaderboard->format);
|
|
rc_format_value(sboard.best_score, sizeof(sboard.best_score), response->best_score, leaderboard->format);
|
|
sboard.new_rank = response->new_rank;
|
|
sboard.num_entries = response->num_entries;
|
|
sboard.num_top_entries = response->num_top_entries;
|
|
if (sboard.num_top_entries > 0) {
|
|
sboard.top_entries = (rc_client_leaderboard_scoreboard_entry_t*)calloc(
|
|
response->num_top_entries, sizeof(rc_client_leaderboard_scoreboard_entry_t));
|
|
if (sboard.top_entries != NULL) {
|
|
uint32_t i;
|
|
for (i = 0; i < response->num_top_entries; i++) {
|
|
sboard.top_entries[i].username = response->top_entries[i].username;
|
|
sboard.top_entries[i].rank = response->top_entries[i].rank;
|
|
rc_format_value(sboard.top_entries[i].score, sizeof(sboard.top_entries[i].score), response->top_entries[i].score,
|
|
leaderboard->format);
|
|
}
|
|
}
|
|
}
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SCOREBOARD;
|
|
client_event.leaderboard = &leaderboard->public_;
|
|
client_event.leaderboard_scoreboard = &sboard;
|
|
|
|
lboard_data->client->callbacks.event_handler(&client_event, lboard_data->client);
|
|
|
|
if (sboard.top_entries != NULL) {
|
|
free(sboard.top_entries);
|
|
}
|
|
}
|
|
|
|
static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_submit_leaderboard_entry_callback_data_t* lboard_data =
|
|
(rc_client_submit_leaderboard_entry_callback_data_t*)callback_data;
|
|
rc_api_submit_lboard_entry_response_t submit_lboard_entry_response;
|
|
|
|
int result = rc_api_process_submit_lboard_entry_server_response(&submit_lboard_entry_response, server_response);
|
|
const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &submit_lboard_entry_response.response);
|
|
|
|
if (error_message) {
|
|
if (submit_lboard_entry_response.response.error_message && !rc_client_should_retry(server_response)) {
|
|
/* actual error from server */
|
|
RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s", lboard_data->id, error_message);
|
|
rc_client_raise_server_error_event(lboard_data->client, "submit_lboard_entry", lboard_data->id, result, submit_lboard_entry_response.response.error_message);
|
|
}
|
|
else if (lboard_data->retry_count++ == 0) {
|
|
/* first retry is immediate */
|
|
RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying immediately", lboard_data->id, error_message);
|
|
rc_client_submit_leaderboard_entry_server_call(lboard_data);
|
|
return;
|
|
}
|
|
else {
|
|
/* double wait time between each attempt until we hit a maximum delay of two minutes */
|
|
/* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/
|
|
const uint32_t delay = (lboard_data->retry_count > 8) ? 120 : (1 << (lboard_data->retry_count - 2));
|
|
RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error submitting leaderboard entry %u: %s, retrying in %u seconds", lboard_data->id, error_message, delay);
|
|
|
|
if (!lboard_data->scheduled_callback_data) {
|
|
lboard_data->scheduled_callback_data = (rc_client_scheduled_callback_data_t*)calloc(1, sizeof(*lboard_data->scheduled_callback_data));
|
|
if (!lboard_data->scheduled_callback_data) {
|
|
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);
|
|
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));
|
|
return;
|
|
}
|
|
lboard_data->scheduled_callback_data->callback = rc_client_submit_leaderboard_entry_retry;
|
|
lboard_data->scheduled_callback_data->data = lboard_data;
|
|
lboard_data->scheduled_callback_data->related_id = lboard_data->id;
|
|
}
|
|
|
|
lboard_data->scheduled_callback_data->when =
|
|
lboard_data->client->callbacks.get_time_millisecs(lboard_data->client) + delay * 1000;
|
|
|
|
rc_client_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data);
|
|
|
|
rc_client_update_disconnect_state(lboard_data->client);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
/* raise event for scoreboard */
|
|
if (lboard_data->retry_count < 2) {
|
|
rc_client_raise_scoreboard_event(lboard_data, &submit_lboard_entry_response);
|
|
}
|
|
|
|
/* not currently doing anything with the response */
|
|
if (lboard_data->retry_count) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(lboard_data->client, "Leaderboard %u submission %d completed after %u attempts",
|
|
lboard_data->id, lboard_data->score, lboard_data->retry_count);
|
|
}
|
|
}
|
|
|
|
if (lboard_data->retry_count)
|
|
rc_client_update_disconnect_state(lboard_data->client);
|
|
|
|
if (lboard_data->scheduled_callback_data)
|
|
free(lboard_data->scheduled_callback_data);
|
|
free(lboard_data);
|
|
}
|
|
|
|
static void rc_client_submit_leaderboard_entry_server_call(rc_client_submit_leaderboard_entry_callback_data_t* lboard_data)
|
|
{
|
|
rc_api_submit_lboard_entry_request_t api_params;
|
|
rc_api_request_t request;
|
|
int result;
|
|
|
|
memset(&api_params, 0, sizeof(api_params));
|
|
api_params.username = lboard_data->client->user.username;
|
|
api_params.api_token = lboard_data->client->user.token;
|
|
api_params.leaderboard_id = lboard_data->id;
|
|
api_params.score = lboard_data->score;
|
|
api_params.game_hash = lboard_data->game_hash;
|
|
|
|
result = rc_api_init_submit_lboard_entry_request(&request, &api_params);
|
|
if (result != RC_OK) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(lboard_data->client, "Error constructing submit leaderboard entry for leaderboard %u: %s", lboard_data->id, rc_error_str(result));
|
|
return;
|
|
}
|
|
|
|
lboard_data->client->callbacks.server_call(&request, rc_client_submit_leaderboard_entry_callback, lboard_data, lboard_data->client);
|
|
|
|
rc_api_destroy_request(&request);
|
|
}
|
|
|
|
static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_leaderboard_info_t* leaderboard)
|
|
{
|
|
rc_client_submit_leaderboard_entry_callback_data_t* callback_data;
|
|
|
|
if (!client->state.hardcore) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission not allowed in softcore", leaderboard->public_.id);
|
|
return;
|
|
}
|
|
|
|
if (client->callbacks.can_submit_leaderboard_entry &&
|
|
!client->callbacks.can_submit_leaderboard_entry(leaderboard->public_.id, client)) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Leaderboard %u entry submission blocked by client", leaderboard->public_.id);
|
|
return;
|
|
}
|
|
|
|
/* don't actually submit leaderboard entries when spectating */
|
|
if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s",
|
|
leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title);
|
|
return;
|
|
}
|
|
|
|
callback_data = (rc_client_submit_leaderboard_entry_callback_data_t*)calloc(1, sizeof(*callback_data));
|
|
if (!callback_data) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(client, "Failed to allocate callback data for submitting entry for leaderboard %u", leaderboard->public_.id);
|
|
rc_client_raise_server_error_event(client, "submit_lboard_entry", leaderboard->public_.id, RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY));
|
|
return;
|
|
}
|
|
callback_data->client = client;
|
|
callback_data->id = leaderboard->public_.id;
|
|
callback_data->score = leaderboard->value;
|
|
callback_data->game_hash = client->game->public_.hash;
|
|
callback_data->submit_time = time(NULL);
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Submitting %s (%d) for leaderboard %u: %s",
|
|
leaderboard->public_.tracker_value, leaderboard->value, leaderboard->public_.id, leaderboard->public_.title);
|
|
rc_client_submit_leaderboard_entry_server_call(callback_data);
|
|
}
|
|
|
|
static void rc_client_subset_reset_leaderboards(rc_client_game_info_t* game, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
|
|
rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
|
|
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
rc_lboard_t* lboard = leaderboard->lboard;
|
|
if (!lboard)
|
|
continue;
|
|
|
|
switch (leaderboard->public_.state) {
|
|
case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
|
|
case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
|
|
continue;
|
|
|
|
case RC_CLIENT_LEADERBOARD_STATE_TRACKING:
|
|
rc_client_release_leaderboard_tracker(game, leaderboard);
|
|
/* fallthrough */ /* to default */
|
|
default:
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
|
|
rc_reset_lboard(lboard);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rc_client_reset_leaderboards(rc_client_t* client)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
for (subset = client->game->subsets; subset; subset = subset->next)
|
|
rc_client_subset_reset_leaderboards(client->game, subset);
|
|
}
|
|
|
|
typedef struct rc_client_fetch_leaderboard_entries_callback_data_t {
|
|
rc_client_t* client;
|
|
rc_client_fetch_leaderboard_entries_callback_t callback;
|
|
void* callback_userdata;
|
|
uint32_t leaderboard_id;
|
|
rc_client_async_handle_t async_handle;
|
|
} rc_client_fetch_leaderboard_entries_callback_data_t;
|
|
|
|
static void rc_client_fetch_leaderboard_entries_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_fetch_leaderboard_entries_callback_data_t* lbinfo_callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)callback_data;
|
|
rc_client_t* client = lbinfo_callback_data->client;
|
|
rc_api_fetch_leaderboard_info_response_t lbinfo_response;
|
|
const char* error_message;
|
|
int result;
|
|
|
|
result = rc_client_end_async(client, &lbinfo_callback_data->async_handle);
|
|
if (result) {
|
|
if (result != RC_CLIENT_ASYNC_DESTROYED) {
|
|
RC_CLIENT_LOG_VERBOSE(client, "Fetch leaderbord entries aborted");
|
|
}
|
|
free(lbinfo_callback_data);
|
|
return;
|
|
}
|
|
|
|
result = rc_api_process_fetch_leaderboard_info_server_response(&lbinfo_response, server_response);
|
|
error_message = rc_client_server_error_message(&result, server_response->http_status_code, &lbinfo_response.response);
|
|
if (error_message) {
|
|
RC_CLIENT_LOG_ERR_FORMATTED(client, "Fetch leaderboard %u info failed: %s", lbinfo_callback_data->leaderboard_id, error_message);
|
|
lbinfo_callback_data->callback(result, error_message, NULL, client, lbinfo_callback_data->callback_userdata);
|
|
}
|
|
else {
|
|
rc_client_leaderboard_entry_list_info_t* info;
|
|
const size_t list_size = sizeof(*info) + sizeof(rc_client_leaderboard_entry_t) * lbinfo_response.num_entries;
|
|
size_t needed_size = list_size;
|
|
uint32_t i;
|
|
|
|
for (i = 0; i < lbinfo_response.num_entries; i++)
|
|
needed_size += strlen(lbinfo_response.entries[i].username) + 1;
|
|
|
|
info = (rc_client_leaderboard_entry_list_info_t*)malloc(needed_size);
|
|
if (!info) {
|
|
lbinfo_callback_data->callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, lbinfo_callback_data->callback_userdata);
|
|
}
|
|
else {
|
|
rc_client_leaderboard_entry_list_t* list = &info->public_;
|
|
rc_client_leaderboard_entry_t* entry = list->entries = (rc_client_leaderboard_entry_t*)((uint8_t*)info + sizeof(*info));
|
|
char* user = (char*)((uint8_t*)list + list_size);
|
|
const rc_api_lboard_info_entry_t* lbentry = lbinfo_response.entries;
|
|
const rc_api_lboard_info_entry_t* stop = lbentry + lbinfo_response.num_entries;
|
|
const size_t logged_in_user_len = strlen(client->user.display_name) + 1;
|
|
info->destroy_func = NULL;
|
|
list->user_index = -1;
|
|
|
|
for (; lbentry < stop; ++lbentry, ++entry) {
|
|
const size_t len = strlen(lbentry->username) + 1;
|
|
entry->user = user;
|
|
memcpy(user, lbentry->username, len);
|
|
user += len;
|
|
|
|
if (len == logged_in_user_len && memcmp(entry->user, client->user.display_name, len) == 0)
|
|
list->user_index = (int)(entry - list->entries);
|
|
|
|
entry->index = lbentry->index;
|
|
entry->rank = lbentry->rank;
|
|
entry->submitted = lbentry->submitted;
|
|
|
|
rc_format_value(entry->display, sizeof(entry->display), lbentry->score, lbinfo_response.format);
|
|
}
|
|
|
|
list->num_entries = lbinfo_response.num_entries;
|
|
|
|
lbinfo_callback_data->callback(RC_OK, NULL, list, client, lbinfo_callback_data->callback_userdata);
|
|
}
|
|
}
|
|
|
|
rc_api_destroy_fetch_leaderboard_info_response(&lbinfo_response);
|
|
free(lbinfo_callback_data);
|
|
}
|
|
|
|
static rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_info(rc_client_t* client,
|
|
const rc_api_fetch_leaderboard_info_request_t* lbinfo_request,
|
|
rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_client_fetch_leaderboard_entries_callback_data_t* callback_data;
|
|
rc_client_async_handle_t* async_handle;
|
|
rc_api_request_t request;
|
|
int result;
|
|
const char* error_message;
|
|
|
|
result = rc_api_init_fetch_leaderboard_info_request(&request, lbinfo_request);
|
|
|
|
if (result != RC_OK) {
|
|
error_message = rc_error_str(result);
|
|
callback(result, error_message, NULL, client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
callback_data = (rc_client_fetch_leaderboard_entries_callback_data_t*)calloc(1, sizeof(*callback_data));
|
|
if (!callback_data) {
|
|
callback(RC_OUT_OF_MEMORY, rc_error_str(RC_OUT_OF_MEMORY), NULL, client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
callback_data->client = client;
|
|
callback_data->callback = callback;
|
|
callback_data->callback_userdata = callback_userdata;
|
|
callback_data->leaderboard_id = lbinfo_request->leaderboard_id;
|
|
|
|
async_handle = &callback_data->async_handle;
|
|
rc_client_begin_async(client, async_handle);
|
|
client->callbacks.server_call(&request, rc_client_fetch_leaderboard_entries_callback, callback_data, client);
|
|
rc_api_destroy_request(&request);
|
|
|
|
return rc_client_async_handle_valid(client, async_handle) ? async_handle : NULL;
|
|
}
|
|
|
|
rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries(rc_client_t* client, uint32_t leaderboard_id,
|
|
uint32_t first_entry, uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_api_fetch_leaderboard_info_request_t lbinfo_request;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries)
|
|
return client->state.external_client->begin_fetch_leaderboard_entries(client, leaderboard_id, first_entry, count, callback, callback_userdata);
|
|
#endif
|
|
|
|
memset(&lbinfo_request, 0, sizeof(lbinfo_request));
|
|
lbinfo_request.leaderboard_id = leaderboard_id;
|
|
lbinfo_request.first_entry = first_entry;
|
|
lbinfo_request.count = count;
|
|
|
|
return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata);
|
|
}
|
|
|
|
rc_client_async_handle_t* rc_client_begin_fetch_leaderboard_entries_around_user(rc_client_t* client, uint32_t leaderboard_id,
|
|
uint32_t count, rc_client_fetch_leaderboard_entries_callback_t callback, void* callback_userdata)
|
|
{
|
|
rc_api_fetch_leaderboard_info_request_t lbinfo_request;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->begin_fetch_leaderboard_entries_around_user)
|
|
return client->state.external_client->begin_fetch_leaderboard_entries_around_user(client, leaderboard_id, count, callback, callback_userdata);
|
|
#endif
|
|
|
|
memset(&lbinfo_request, 0, sizeof(lbinfo_request));
|
|
lbinfo_request.leaderboard_id = leaderboard_id;
|
|
lbinfo_request.username = client->user.username;
|
|
lbinfo_request.count = count;
|
|
|
|
if (!lbinfo_request.username) {
|
|
callback(RC_LOGIN_REQUIRED, rc_error_str(RC_LOGIN_REQUIRED), NULL, client, callback_userdata);
|
|
return NULL;
|
|
}
|
|
|
|
return rc_client_begin_fetch_leaderboard_info(client, &lbinfo_request, callback, callback_userdata);
|
|
}
|
|
|
|
void rc_client_destroy_leaderboard_entry_list(rc_client_leaderboard_entry_list_t* list)
|
|
{
|
|
rc_client_leaderboard_entry_list_info_t* info = (rc_client_leaderboard_entry_list_info_t*)list;
|
|
if (info->destroy_func)
|
|
info->destroy_func(info);
|
|
else
|
|
free(list);
|
|
}
|
|
|
|
int rc_client_leaderboard_entry_get_user_image_url(const rc_client_leaderboard_entry_t* entry, char buffer[], size_t buffer_size)
|
|
{
|
|
if (!entry)
|
|
return RC_INVALID_STATE;
|
|
|
|
return rc_client_get_image_url(buffer, buffer_size, RC_IMAGE_TYPE_USER, entry->user);
|
|
}
|
|
|
|
/* ===== Rich Presence ===== */
|
|
|
|
static void rc_client_ping_callback(const rc_api_server_response_t* server_response, void* callback_data)
|
|
{
|
|
rc_client_t* client = (rc_client_t*)callback_data;
|
|
rc_api_ping_response_t response;
|
|
|
|
int result = rc_api_process_ping_server_response(&response, server_response);
|
|
const char* error_message = rc_client_server_error_message(&result, server_response->http_status_code, &response.response);
|
|
if (error_message) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(client, "Ping response error: %s", error_message);
|
|
}
|
|
|
|
rc_api_destroy_ping_response(&response);
|
|
}
|
|
|
|
static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
|
|
{
|
|
rc_api_ping_request_t api_params;
|
|
rc_api_request_t request;
|
|
char buffer[256];
|
|
int result;
|
|
|
|
if (!client->callbacks.rich_presence_override ||
|
|
!client->callbacks.rich_presence_override(client, buffer, sizeof(buffer))) {
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
rc_runtime_get_richpresence(&client->game->runtime, buffer, sizeof(buffer),
|
|
client->state.legacy_peek, client, NULL);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
memset(&api_params, 0, sizeof(api_params));
|
|
api_params.username = client->user.username;
|
|
api_params.api_token = client->user.token;
|
|
api_params.game_id = client->game->public_.id;
|
|
api_params.rich_presence = buffer;
|
|
api_params.game_hash = client->game->public_.hash;
|
|
api_params.hardcore = client->state.hardcore;
|
|
|
|
result = rc_api_init_ping_request(&request, &api_params);
|
|
if (result != RC_OK) {
|
|
RC_CLIENT_LOG_WARN_FORMATTED(client, "Error generating ping request: %s", rc_error_str(result));
|
|
}
|
|
else {
|
|
client->callbacks.server_call(&request, rc_client_ping_callback, client, client);
|
|
}
|
|
|
|
callback_data->when = now + 120 * 1000;
|
|
rc_client_schedule_callback(client, callback_data);
|
|
}
|
|
|
|
int rc_client_has_rich_presence(rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->has_rich_presence)
|
|
return client->state.external_client->has_rich_presence();
|
|
#endif
|
|
|
|
if (!client->game || !client->game->runtime.richpresence || !client->game->runtime.richpresence->richpresence)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
size_t rc_client_get_rich_presence_message(rc_client_t* client, char buffer[], size_t buffer_size)
|
|
{
|
|
int result;
|
|
|
|
if (!client || !buffer)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_rich_presence_message)
|
|
return client->state.external_client->get_rich_presence_message(buffer, buffer_size);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return 0;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
result = rc_runtime_get_richpresence(&client->game->runtime, buffer, (unsigned)buffer_size,
|
|
client->state.legacy_peek, client, NULL);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (result == 0) {
|
|
result = snprintf(buffer, buffer_size, "Playing %s", client->game->public_.title);
|
|
/* snprintf will return the amount of space needed, we want to return the number of chars written */
|
|
if ((size_t)result >= buffer_size)
|
|
return (buffer_size - 1);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/* ===== Processing ===== */
|
|
|
|
void rc_client_set_event_handler(rc_client_t* client, rc_client_event_handler_t handler)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->set_event_handler)
|
|
client->state.external_client->set_event_handler(client, handler);
|
|
#endif
|
|
|
|
client->callbacks.event_handler = handler;
|
|
}
|
|
|
|
void rc_client_set_read_memory_function(rc_client_t* client, rc_client_read_memory_func_t handler)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->set_read_memory)
|
|
client->state.external_client->set_read_memory(client, handler);
|
|
#endif
|
|
|
|
client->callbacks.read_memory = handler;
|
|
}
|
|
|
|
static void rc_client_invalidate_processing_memref(rc_client_t* client)
|
|
{
|
|
rc_memref_t** next_memref = &client->game->runtime.memrefs;
|
|
rc_memref_t* memref;
|
|
|
|
/* if processing_memref is not set, this occurred following a pointer chain. ignore it. */
|
|
if (!client->state.processing_memref)
|
|
return;
|
|
|
|
/* invalid memref. remove from chain so we don't have to evaluate it in the future.
|
|
* it's still there, so anything referencing it will always fetch the current value. */
|
|
while ((memref = *next_memref) != NULL) {
|
|
if (memref == client->state.processing_memref) {
|
|
*next_memref = memref->next;
|
|
break;
|
|
}
|
|
next_memref = &memref->next;
|
|
}
|
|
|
|
rc_client_invalidate_memref_achievements(client->game, client, client->state.processing_memref);
|
|
rc_client_invalidate_memref_leaderboards(client->game, client, client->state.processing_memref);
|
|
|
|
client->state.processing_memref = NULL;
|
|
}
|
|
|
|
static uint32_t rc_client_peek_le(uint32_t address, uint32_t num_bytes, void* ud)
|
|
{
|
|
rc_client_t* client = (rc_client_t*)ud;
|
|
uint32_t value = 0;
|
|
uint32_t num_read = 0;
|
|
|
|
/* if we know the address is out of range, and it's part of a pointer chain
|
|
* (processing_memref is null), don't bother processing it. */
|
|
if (address > client->game->max_valid_address && !client->state.processing_memref)
|
|
return 0;
|
|
|
|
if (num_bytes <= sizeof(value)) {
|
|
num_read = client->callbacks.read_memory(address, (uint8_t*)&value, num_bytes, client);
|
|
if (num_read == num_bytes)
|
|
return value;
|
|
}
|
|
|
|
if (num_read < num_bytes)
|
|
rc_client_invalidate_processing_memref(client);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint32_t rc_client_peek(uint32_t address, uint32_t num_bytes, void* ud)
|
|
{
|
|
rc_client_t* client = (rc_client_t*)ud;
|
|
uint8_t buffer[4];
|
|
uint32_t num_read = 0;
|
|
|
|
/* if we know the address is out of range, and it's part of a pointer chain
|
|
* (processing_memref is null), don't bother processing it. */
|
|
if (address > client->game->max_valid_address && !client->state.processing_memref)
|
|
return 0;
|
|
|
|
switch (num_bytes) {
|
|
case 1:
|
|
num_read = client->callbacks.read_memory(address, buffer, 1, client);
|
|
if (num_read == 1)
|
|
return buffer[0];
|
|
break;
|
|
case 2:
|
|
num_read = client->callbacks.read_memory(address, buffer, 2, client);
|
|
if (num_read == 2)
|
|
return buffer[0] | (buffer[1] << 8);
|
|
break;
|
|
case 3:
|
|
num_read = client->callbacks.read_memory(address, buffer, 3, client);
|
|
if (num_read == 3)
|
|
return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16);
|
|
break;
|
|
case 4:
|
|
num_read = client->callbacks.read_memory(address, buffer, 4, client);
|
|
if (num_read == 4)
|
|
return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (num_read < num_bytes)
|
|
rc_client_invalidate_processing_memref(client);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void rc_client_set_legacy_peek(rc_client_t* client, int method)
|
|
{
|
|
if (method == RC_CLIENT_LEGACY_PEEK_AUTO) {
|
|
union {
|
|
uint32_t whole;
|
|
uint8_t parts[4];
|
|
} u;
|
|
u.whole = 1;
|
|
method = (u.parts[0] == 1) ?
|
|
RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED;
|
|
}
|
|
|
|
client->state.legacy_peek = (method == RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS) ?
|
|
rc_client_peek_le : rc_client_peek;
|
|
}
|
|
|
|
int rc_client_is_processing_required(rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->is_processing_required)
|
|
return client->state.external_client->is_processing_required();
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return 0;
|
|
|
|
if (client->game->runtime.trigger_count || client->game->runtime.lboard_count)
|
|
return 1;
|
|
|
|
return (client->game->runtime.richpresence && client->game->runtime.richpresence->richpresence);
|
|
}
|
|
|
|
static void rc_client_update_memref_values(rc_client_t* client)
|
|
{
|
|
rc_memref_t* memref = client->game->runtime.memrefs;
|
|
uint32_t value;
|
|
int invalidated_memref = 0;
|
|
|
|
for (; memref; memref = memref->next) {
|
|
if (memref->value.is_indirect)
|
|
continue;
|
|
|
|
client->state.processing_memref = memref;
|
|
|
|
value = rc_peek_value(memref->address, memref->value.size, client->state.legacy_peek, client);
|
|
|
|
if (client->state.processing_memref) {
|
|
rc_update_memref_value(&memref->value, value);
|
|
}
|
|
else {
|
|
/* if the peek function cleared the processing_memref, the memref was invalidated */
|
|
invalidated_memref = 1;
|
|
}
|
|
}
|
|
|
|
client->state.processing_memref = NULL;
|
|
|
|
if (invalidated_memref)
|
|
rc_client_update_active_achievements(client->game);
|
|
}
|
|
|
|
static void rc_client_do_frame_process_achievements(rc_client_t* client, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
rc_trigger_t* trigger = achievement->trigger;
|
|
int old_state, new_state;
|
|
uint32_t old_measured_value;
|
|
|
|
if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
|
|
continue;
|
|
|
|
old_measured_value = trigger->measured_value;
|
|
old_state = trigger->state;
|
|
new_state = rc_evaluate_trigger(trigger, client->state.legacy_peek, client, NULL);
|
|
|
|
/* trigger->state doesn't actually change to RESET - RESET just serves as a notification.
|
|
* we don't care about that particular notification, so look at the actual state. */
|
|
if (new_state == RC_TRIGGER_STATE_RESET)
|
|
new_state = trigger->state;
|
|
|
|
/* if the measured value changed and the achievement hasn't triggered, show a progress indicator */
|
|
if (trigger->measured_value != old_measured_value && old_measured_value != RC_MEASURED_UNKNOWN &&
|
|
trigger->measured_value <= trigger->measured_target &&
|
|
rc_trigger_state_active(new_state) && new_state != RC_TRIGGER_STATE_WAITING) {
|
|
|
|
/* only show a popup for the achievement closest to triggering */
|
|
float progress = (float)trigger->measured_value / (float)trigger->measured_target;
|
|
|
|
if (trigger->measured_as_percent) {
|
|
/* if reporting the measured value as a percentage, only show the popup if the percentage changes */
|
|
const uint32_t old_percent = (uint32_t)(((unsigned long long)old_measured_value * 100) / trigger->measured_target);
|
|
const uint32_t new_percent = (uint32_t)(((unsigned long long)trigger->measured_value * 100) / trigger->measured_target);
|
|
if (old_percent == new_percent)
|
|
progress = -1.0;
|
|
}
|
|
|
|
if (progress > client->game->progress_tracker.progress) {
|
|
client->game->progress_tracker.progress = progress;
|
|
client->game->progress_tracker.achievement = achievement;
|
|
client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER;
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_UPDATE;
|
|
}
|
|
}
|
|
|
|
/* if the state hasn't changed, there won't be any events raised */
|
|
if (new_state == old_state)
|
|
continue;
|
|
|
|
/* raise a CHALLENGE_INDICATOR_HIDE event when changing from PRIMED to anything else */
|
|
if (old_state == RC_TRIGGER_STATE_PRIMED)
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
|
|
|
|
/* raise events for each of the possible new states */
|
|
if (new_state == RC_TRIGGER_STATE_TRIGGERED)
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED;
|
|
else if (new_state == RC_TRIGGER_STATE_PRIMED)
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW;
|
|
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
|
|
}
|
|
}
|
|
|
|
static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game)
|
|
{
|
|
/* ASSERT: this should only be called if the mutex is held */
|
|
|
|
if (game->progress_tracker.hide_callback &&
|
|
game->progress_tracker.hide_callback->when &&
|
|
game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) {
|
|
rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, 0);
|
|
game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE;
|
|
game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER;
|
|
}
|
|
}
|
|
|
|
static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now)
|
|
{
|
|
rc_client_event_t client_event;
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
|
|
(void)callback_data;
|
|
(void)now;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
if (client->game->progress_tracker.action == RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE) {
|
|
client->game->progress_tracker.hide_callback->when = 0;
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE;
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (client_event.type)
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_client_game_info_t* game)
|
|
{
|
|
if (!game->progress_tracker.hide_callback) {
|
|
game->progress_tracker.hide_callback = (rc_client_scheduled_callback_data_t*)
|
|
rc_buffer_alloc(&game->buffer, sizeof(rc_client_scheduled_callback_data_t));
|
|
memset(game->progress_tracker.hide_callback, 0, sizeof(rc_client_scheduled_callback_data_t));
|
|
game->progress_tracker.hide_callback->callback = rc_client_progress_tracker_timer_elapsed;
|
|
}
|
|
|
|
if (game->progress_tracker.hide_callback->when == 0)
|
|
game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW;
|
|
else
|
|
game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE;
|
|
|
|
rc_client_reschedule_callback(client, game->progress_tracker.hide_callback,
|
|
client->callbacks.get_time_millisecs(client) + 2 * 1000);
|
|
}
|
|
|
|
static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game)
|
|
{
|
|
rc_client_event_t client_event;
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
|
|
switch (game->progress_tracker.action) {
|
|
case RC_CLIENT_PROGRESS_TRACKER_ACTION_SHOW:
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_SHOW;
|
|
break;
|
|
case RC_CLIENT_PROGRESS_TRACKER_ACTION_HIDE:
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_HIDE;
|
|
break;
|
|
default:
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_PROGRESS_INDICATOR_UPDATE;
|
|
break;
|
|
}
|
|
game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_NONE;
|
|
|
|
client_event.achievement = &game->progress_tracker.achievement->public_;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
static void rc_client_raise_achievement_events(rc_client_t* client, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_achievement_info_t* achievement = subset->achievements;
|
|
rc_client_achievement_info_t* stop = achievement + subset->public_.num_achievements;
|
|
rc_client_event_t client_event;
|
|
time_t recent_unlock_time = 0;
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
|
|
for (; achievement < stop; ++achievement) {
|
|
if (achievement->pending_events == RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE)
|
|
continue;
|
|
|
|
/* kick off award achievement request first */
|
|
if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) {
|
|
rc_client_award_achievement(client, achievement);
|
|
client->game->pending_events |= RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS;
|
|
}
|
|
|
|
/* update display state */
|
|
if (recent_unlock_time == 0)
|
|
recent_unlock_time = time(NULL) - RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS;
|
|
rc_client_update_achievement_display_information(client, achievement, recent_unlock_time);
|
|
|
|
/* raise events */
|
|
client_event.achievement = &achievement->public_;
|
|
|
|
if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) {
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_HIDE;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
else if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW) {
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_CHALLENGE_INDICATOR_SHOW;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_TRIGGERED) {
|
|
client_event.type = RC_CLIENT_EVENT_ACHIEVEMENT_TRIGGERED;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
/* clear pending flags */
|
|
achievement->pending_events = RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_NONE;
|
|
}
|
|
}
|
|
|
|
static void rc_client_raise_mastery_event(rc_client_t* client, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_event_t client_event;
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
client_event.type = RC_CLIENT_EVENT_GAME_COMPLETED;
|
|
|
|
subset->mastery = RC_CLIENT_MASTERY_STATE_SHOWN;
|
|
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
static void rc_client_do_frame_process_leaderboards(rc_client_t* client, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
|
|
rc_client_leaderboard_info_t* stop = leaderboard + subset->public_.num_leaderboards;
|
|
|
|
for (; leaderboard < stop; ++leaderboard) {
|
|
rc_lboard_t* lboard = leaderboard->lboard;
|
|
int old_state, new_state;
|
|
|
|
switch (leaderboard->public_.state) {
|
|
case RC_CLIENT_LEADERBOARD_STATE_INACTIVE:
|
|
case RC_CLIENT_LEADERBOARD_STATE_DISABLED:
|
|
continue;
|
|
|
|
default:
|
|
if (!lboard)
|
|
continue;
|
|
|
|
break;
|
|
}
|
|
|
|
old_state = lboard->state;
|
|
new_state = rc_evaluate_lboard(lboard, &leaderboard->value, client->state.legacy_peek, client, NULL);
|
|
|
|
switch (new_state) {
|
|
case RC_LBOARD_STATE_STARTED: /* leaderboard is running */
|
|
if (old_state != RC_LBOARD_STATE_STARTED) {
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING;
|
|
leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED;
|
|
rc_client_allocate_leaderboard_tracker(client->game, leaderboard);
|
|
}
|
|
else {
|
|
rc_client_update_leaderboard_tracker(client->game, leaderboard);
|
|
}
|
|
break;
|
|
|
|
case RC_LBOARD_STATE_CANCELED:
|
|
if (old_state != RC_LBOARD_STATE_CANCELED) {
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
|
|
leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
|
|
rc_client_release_leaderboard_tracker(client->game, leaderboard);
|
|
}
|
|
break;
|
|
|
|
case RC_LBOARD_STATE_TRIGGERED:
|
|
if (old_state != RC_RUNTIME_EVENT_LBOARD_TRIGGERED) {
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
|
|
leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED;
|
|
|
|
if (old_state != RC_LBOARD_STATE_STARTED)
|
|
rc_client_allocate_leaderboard_tracker(client->game, leaderboard);
|
|
else
|
|
rc_client_update_leaderboard_tracker(client->game, leaderboard);
|
|
|
|
rc_client_release_leaderboard_tracker(client->game, leaderboard);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (leaderboard->pending_events)
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD;
|
|
}
|
|
}
|
|
|
|
static void rc_client_raise_leaderboard_tracker_events(rc_client_t* client, rc_client_game_info_t* game)
|
|
{
|
|
rc_client_leaderboard_tracker_info_t* tracker = game->leaderboard_trackers;
|
|
rc_client_event_t client_event;
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
|
|
tracker = game->leaderboard_trackers;
|
|
for (; tracker; tracker = tracker->next) {
|
|
if (tracker->pending_events == RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE)
|
|
continue;
|
|
|
|
client_event.leaderboard_tracker = &tracker->public_;
|
|
|
|
/* update display text for new trackers or updated trackers */
|
|
if (tracker->pending_events & (RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW | RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE))
|
|
rc_format_value(tracker->public_.display, sizeof(tracker->public_.display), tracker->raw_value, tracker->format);
|
|
|
|
if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_HIDE) {
|
|
if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) {
|
|
/* request to show and hide in the same frame - ignore the event */
|
|
}
|
|
else {
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_HIDE;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
}
|
|
else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_SHOW) {
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_SHOW;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
else if (tracker->pending_events & RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_UPDATE) {
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
tracker->pending_events = RC_CLIENT_LEADERBOARD_TRACKER_PENDING_EVENT_NONE;
|
|
}
|
|
}
|
|
|
|
static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_leaderboard_info_t* leaderboard = subset->leaderboards;
|
|
rc_client_leaderboard_info_t* leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
|
|
rc_client_event_t client_event;
|
|
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
|
|
for (; leaderboard < leaderboard_stop; ++leaderboard) {
|
|
if (leaderboard->pending_events == RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE)
|
|
continue;
|
|
|
|
client_event.leaderboard = &leaderboard->public_;
|
|
|
|
if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) {
|
|
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u canceled: %s", leaderboard->public_.id, leaderboard->public_.title);
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_FAILED;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_SUBMITTED) {
|
|
/* kick off submission request before raising event */
|
|
rc_client_submit_leaderboard_entry(client, leaderboard);
|
|
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_SUBMITTED;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_STARTED) {
|
|
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Leaderboard %u started: %s", leaderboard->public_.id, leaderboard->public_.title);
|
|
client_event.type = RC_CLIENT_EVENT_LEADERBOARD_STARTED;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
|
|
leaderboard->pending_events = RC_CLIENT_LEADERBOARD_PENDING_EVENT_NONE;
|
|
}
|
|
}
|
|
|
|
static void rc_client_reset_pending_events(rc_client_t* client)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
|
|
client->game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE;
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next)
|
|
subset->pending_events = RC_CLIENT_SUBSET_PENDING_EVENT_NONE;
|
|
}
|
|
|
|
static void rc_client_subset_raise_pending_events(rc_client_t* client, rc_client_subset_info_t* subset)
|
|
{
|
|
/* raise any pending achievement events */
|
|
if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT)
|
|
rc_client_raise_achievement_events(client, subset);
|
|
|
|
/* raise any pending leaderboard events */
|
|
if (subset->pending_events & RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD)
|
|
rc_client_raise_leaderboard_events(client, subset);
|
|
|
|
/* raise mastery event if pending */
|
|
if (subset->mastery == RC_CLIENT_MASTERY_STATE_PENDING)
|
|
rc_client_raise_mastery_event(client, subset);
|
|
}
|
|
|
|
static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
|
|
/* raise tracker events before leaderboard events so formatted values are updated for leaderboard events */
|
|
if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_LEADERBOARD_TRACKER)
|
|
rc_client_raise_leaderboard_tracker_events(client, game);
|
|
|
|
for (subset = game->subsets; subset; subset = subset->next)
|
|
rc_client_subset_raise_pending_events(client, subset);
|
|
|
|
/* raise progress tracker events after achievement events so formatted values are updated for tracker event */
|
|
if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER)
|
|
rc_client_raise_progress_tracker_events(client, game);
|
|
|
|
/* if any achievements were unlocked, resync the active achievements list */
|
|
if (game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_UPDATE_ACTIVE_ACHIEVEMENTS) {
|
|
rc_mutex_lock(&client->state.mutex);
|
|
rc_client_update_active_achievements(game);
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
game->pending_events = RC_CLIENT_GAME_PENDING_EVENT_NONE;
|
|
}
|
|
|
|
void rc_client_do_frame(rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->do_frame) {
|
|
client->state.external_client->do_frame();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (client->game && !client->game->waiting_for_reset) {
|
|
rc_runtime_richpresence_t* richpresence;
|
|
rc_client_subset_info_t* subset;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
rc_client_reset_pending_events(client);
|
|
|
|
rc_client_update_memref_values(client);
|
|
rc_update_variables(client->game->runtime.variables, client->state.legacy_peek, client, NULL);
|
|
|
|
client->game->progress_tracker.progress = 0.0;
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (subset->active)
|
|
rc_client_do_frame_process_achievements(client, subset);
|
|
}
|
|
if (client->game->pending_events & RC_CLIENT_GAME_PENDING_EVENT_PROGRESS_TRACKER)
|
|
rc_client_do_frame_update_progress_tracker(client, client->game);
|
|
|
|
if (client->state.hardcore || client->state.allow_leaderboards_in_softcore) {
|
|
for (subset = client->game->subsets; subset; subset = subset->next) {
|
|
if (subset->active)
|
|
rc_client_do_frame_process_leaderboards(client, subset);
|
|
}
|
|
}
|
|
|
|
richpresence = client->game->runtime.richpresence;
|
|
if (richpresence && richpresence->richpresence)
|
|
rc_update_richpresence(richpresence->richpresence, client->state.legacy_peek, client, NULL);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
rc_client_raise_pending_events(client, client->game);
|
|
}
|
|
|
|
/* we've processed a frame. if there's a pause delay in effect, process it */
|
|
if (client->state.unpaused_frame_decay > 0) {
|
|
client->state.unpaused_frame_decay--;
|
|
|
|
if (client->state.unpaused_frame_decay == 0 &&
|
|
client->state.required_unpaused_frames > RC_MINIMUM_UNPAUSED_FRAMES) {
|
|
/* the full decay has elapsed and a penalty still exists.
|
|
* lower the penalty and reset the decay counter */
|
|
client->state.required_unpaused_frames >>= 1;
|
|
|
|
if (client->state.required_unpaused_frames <= RC_MINIMUM_UNPAUSED_FRAMES)
|
|
client->state.required_unpaused_frames = RC_MINIMUM_UNPAUSED_FRAMES;
|
|
|
|
client->state.unpaused_frame_decay =
|
|
client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1) - 1;
|
|
}
|
|
}
|
|
|
|
rc_client_idle(client);
|
|
}
|
|
|
|
void rc_client_idle(rc_client_t* client)
|
|
{
|
|
rc_client_scheduled_callback_data_t* scheduled_callback;
|
|
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->idle) {
|
|
client->state.external_client->idle();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
scheduled_callback = client->state.scheduled_callbacks;
|
|
if (scheduled_callback) {
|
|
const rc_clock_t now = client->callbacks.get_time_millisecs(client);
|
|
|
|
do {
|
|
rc_mutex_lock(&client->state.mutex);
|
|
scheduled_callback = client->state.scheduled_callbacks;
|
|
if (scheduled_callback) {
|
|
if (scheduled_callback->when > now) {
|
|
/* not time for next callback yet, ignore it */
|
|
scheduled_callback = NULL;
|
|
}
|
|
else {
|
|
/* remove the callback from the queue while we process it. callback can requeue if desired */
|
|
client->state.scheduled_callbacks = scheduled_callback->next;
|
|
}
|
|
}
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
if (!scheduled_callback)
|
|
break;
|
|
|
|
scheduled_callback->callback(scheduled_callback, client, now);
|
|
} while (1);
|
|
}
|
|
|
|
if (client->state.disconnect & ~RC_CLIENT_DISCONNECT_VISIBLE)
|
|
rc_client_raise_disconnect_events(client);
|
|
}
|
|
|
|
void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback)
|
|
{
|
|
rc_client_scheduled_callback_data_t** last;
|
|
rc_client_scheduled_callback_data_t* next;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
last = &client->state.scheduled_callbacks;
|
|
do {
|
|
next = *last;
|
|
if (!next || scheduled_callback->when < next->when) {
|
|
scheduled_callback->next = next;
|
|
*last = scheduled_callback;
|
|
break;
|
|
}
|
|
|
|
last = &next->next;
|
|
} while (1);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
}
|
|
|
|
static void rc_client_reschedule_callback(rc_client_t* client,
|
|
rc_client_scheduled_callback_data_t* callback, rc_clock_t when)
|
|
{
|
|
rc_client_scheduled_callback_data_t** last;
|
|
rc_client_scheduled_callback_data_t* next;
|
|
|
|
/* ASSERT: this should only be called if the mutex is held */
|
|
|
|
callback->when = when;
|
|
|
|
last = &client->state.scheduled_callbacks;
|
|
do {
|
|
next = *last;
|
|
|
|
if (next == callback) {
|
|
if (when == 0) {
|
|
/* request to unschedule the callback */
|
|
*last = next->next;
|
|
next->next = NULL;
|
|
break;
|
|
}
|
|
|
|
if (!next->next) {
|
|
/* end of list, just append it */
|
|
break;
|
|
}
|
|
|
|
if (when < next->next->when) {
|
|
/* already in the correct place */
|
|
break;
|
|
}
|
|
|
|
/* remove from current position - will insert later */
|
|
*last = next->next;
|
|
next->next = NULL;
|
|
continue;
|
|
}
|
|
|
|
if (!next || when < next->when) {
|
|
/* insert here */
|
|
callback->next = next;
|
|
*last = callback;
|
|
break;
|
|
}
|
|
|
|
last = &next->next;
|
|
} while (1);
|
|
}
|
|
|
|
static void rc_client_reset_richpresence(rc_client_t* client)
|
|
{
|
|
rc_runtime_richpresence_t* richpresence = client->game->runtime.richpresence;
|
|
if (richpresence && richpresence->richpresence)
|
|
rc_reset_richpresence(richpresence->richpresence);
|
|
}
|
|
|
|
static void rc_client_reset_variables(rc_client_t* client)
|
|
{
|
|
rc_value_t* variable = client->game->runtime.variables;
|
|
for (; variable; variable = variable->next)
|
|
rc_reset_value(variable);
|
|
}
|
|
|
|
static void rc_client_reset_all(rc_client_t* client)
|
|
{
|
|
rc_client_reset_achievements(client);
|
|
rc_client_reset_leaderboards(client);
|
|
rc_client_reset_richpresence(client);
|
|
rc_client_reset_variables(client);
|
|
}
|
|
|
|
void rc_client_reset(rc_client_t* client)
|
|
{
|
|
rc_client_game_hash_t* game_hash;
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->reset) {
|
|
client->state.external_client->reset();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return;
|
|
|
|
game_hash = rc_client_find_game_hash(client, client->game->public_.hash);
|
|
if (game_hash && game_hash->game_id != client->game->public_.id) {
|
|
/* current media is not for loaded game. unload game */
|
|
RC_CLIENT_LOG_WARN_FORMATTED(client, "Disabling runtime. Reset with non-game media loaded: %u (%s)",
|
|
(game_hash->game_id == RC_CLIENT_UNKNOWN_GAME_ID) ? 0 : game_hash->game_id, game_hash->hash);
|
|
rc_client_unload_game(client);
|
|
return;
|
|
}
|
|
|
|
RC_CLIENT_LOG_INFO(client, "Resetting runtime");
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
client->game->waiting_for_reset = 0;
|
|
rc_client_reset_pending_events(client);
|
|
|
|
rc_client_hide_progress_tracker(client, client->game);
|
|
rc_client_reset_all(client);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
rc_client_raise_pending_events(client, client->game);
|
|
}
|
|
|
|
int rc_client_can_pause(rc_client_t* client, uint32_t* frames_remaining)
|
|
{
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->can_pause)
|
|
return client->state.external_client->can_pause(frames_remaining);
|
|
#endif
|
|
|
|
if (frames_remaining)
|
|
*frames_remaining = 0;
|
|
|
|
/* pause is always allowed in softcore */
|
|
if (!rc_client_get_hardcore_enabled(client))
|
|
return 1;
|
|
|
|
/* a full decay means we haven't processed any frames since the last time this was called. */
|
|
if (client->state.unpaused_frame_decay == client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER)
|
|
return 1;
|
|
|
|
/* if less than RC_MINIMUM_UNPAUSED_FRAMES have been processed, don't allow the pause */
|
|
if (client->state.unpaused_frame_decay > client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1)) {
|
|
if (frames_remaining) {
|
|
*frames_remaining = client->state.unpaused_frame_decay -
|
|
client->state.required_unpaused_frames * (RC_PAUSE_DECAY_MULTIPLIER - 1);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* we're going to allow the emulator to pause. calculate how many frames are needed before the next
|
|
* pause will be allowed. */
|
|
|
|
if (client->state.unpaused_frame_decay > 0) {
|
|
/* The user has paused within the decay window. Require a longer
|
|
* run of unpaused frames before allowing the next pause */
|
|
if (client->state.required_unpaused_frames < 5 * 60) /* don't make delay longer then 5 seconds */
|
|
client->state.required_unpaused_frames += RC_MINIMUM_UNPAUSED_FRAMES;
|
|
}
|
|
|
|
/* require multiple unpaused_frames windows to decay the penalty */
|
|
client->state.unpaused_frame_decay = client->state.required_unpaused_frames * RC_PAUSE_DECAY_MULTIPLIER;
|
|
|
|
return 1;
|
|
}
|
|
|
|
size_t rc_client_progress_size(rc_client_t* client)
|
|
{
|
|
size_t result;
|
|
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->progress_size)
|
|
return client->state.external_client->progress_size();
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return 0;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
result = rc_runtime_progress_size(&client->game->runtime, NULL);
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return result;
|
|
}
|
|
|
|
int rc_client_serialize_progress(rc_client_t* client, uint8_t* buffer)
|
|
{
|
|
int result;
|
|
|
|
if (!client)
|
|
return RC_NO_GAME_LOADED;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->serialize_progress)
|
|
return client->state.external_client->serialize_progress(buffer);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return RC_NO_GAME_LOADED;
|
|
|
|
if (!buffer)
|
|
return RC_INVALID_STATE;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
result = rc_runtime_serialize_progress(buffer, &client->game->runtime, NULL);
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
return result;
|
|
}
|
|
|
|
static void rc_client_subset_before_deserialize_progress(rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_achievement_info_t* achievement;
|
|
rc_client_achievement_info_t* achievement_stop;
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* leaderboard_stop;
|
|
|
|
/* flag any visible challenge indicators to be hidden */
|
|
achievement = subset->achievements;
|
|
achievement_stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < achievement_stop; ++achievement) {
|
|
rc_trigger_t* trigger = achievement->trigger;
|
|
if (trigger && trigger->state == RC_TRIGGER_STATE_PRIMED &&
|
|
achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE) {
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
|
|
}
|
|
}
|
|
|
|
/* flag any visible trackers to be hidden */
|
|
leaderboard = subset->leaderboards;
|
|
leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < leaderboard_stop; ++leaderboard) {
|
|
rc_lboard_t* lboard = leaderboard->lboard;
|
|
if (lboard && lboard->state == RC_LBOARD_STATE_STARTED &&
|
|
leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_TRACKING) {
|
|
leaderboard->pending_events |= RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_LEADERBOARD;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void rc_client_subset_after_deserialize_progress(rc_client_game_info_t* game, rc_client_subset_info_t* subset)
|
|
{
|
|
rc_client_achievement_info_t* achievement;
|
|
rc_client_achievement_info_t* achievement_stop;
|
|
rc_client_leaderboard_info_t* leaderboard;
|
|
rc_client_leaderboard_info_t* leaderboard_stop;
|
|
|
|
/* flag any challenge indicators that should be shown */
|
|
achievement = subset->achievements;
|
|
achievement_stop = achievement + subset->public_.num_achievements;
|
|
for (; achievement < achievement_stop; ++achievement) {
|
|
rc_trigger_t* trigger = achievement->trigger;
|
|
if (!trigger || achievement->public_.state != RC_CLIENT_ACHIEVEMENT_STATE_ACTIVE)
|
|
continue;
|
|
|
|
if (trigger->state == RC_TRIGGER_STATE_PRIMED) {
|
|
/* if it's already shown, just keep it. otherwise flag it to be shown */
|
|
if (achievement->pending_events & RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE) {
|
|
achievement->pending_events &= ~RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_HIDE;
|
|
}
|
|
else {
|
|
achievement->pending_events |= RC_CLIENT_ACHIEVEMENT_PENDING_EVENT_CHALLENGE_INDICATOR_SHOW;
|
|
subset->pending_events |= RC_CLIENT_SUBSET_PENDING_EVENT_ACHIEVEMENT;
|
|
}
|
|
}
|
|
/* ASSERT: only active achievements are serialized, so we don't have to worry about
|
|
* deserialization deactiving them. */
|
|
}
|
|
|
|
/* flag any trackers that need to be shown */
|
|
leaderboard = subset->leaderboards;
|
|
leaderboard_stop = leaderboard + subset->public_.num_leaderboards;
|
|
for (; leaderboard < leaderboard_stop; ++leaderboard) {
|
|
rc_lboard_t* lboard = leaderboard->lboard;
|
|
if (!lboard ||
|
|
leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_INACTIVE ||
|
|
leaderboard->public_.state == RC_CLIENT_LEADERBOARD_STATE_DISABLED)
|
|
continue;
|
|
|
|
if (lboard->state == RC_LBOARD_STATE_STARTED) {
|
|
leaderboard->value = (int)lboard->value.value.value;
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_TRACKING;
|
|
|
|
/* if it's already being tracked, just update tracker. otherwise, allocate one */
|
|
if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) {
|
|
leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
|
|
rc_client_update_leaderboard_tracker(game, leaderboard);
|
|
}
|
|
else {
|
|
rc_client_allocate_leaderboard_tracker(game, leaderboard);
|
|
}
|
|
}
|
|
else if (leaderboard->pending_events & RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED) {
|
|
/* deallocate the tracker (don't actually raise the failed event) */
|
|
leaderboard->pending_events &= ~RC_CLIENT_LEADERBOARD_PENDING_EVENT_FAILED;
|
|
leaderboard->public_.state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE;
|
|
rc_client_release_leaderboard_tracker(game, leaderboard);
|
|
}
|
|
}
|
|
}
|
|
|
|
int rc_client_deserialize_progress(rc_client_t* client, const uint8_t* serialized)
|
|
{
|
|
rc_client_subset_info_t* subset;
|
|
int result;
|
|
|
|
if (!client)
|
|
return RC_NO_GAME_LOADED;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->deserialize_progress)
|
|
return client->state.external_client->deserialize_progress(serialized);
|
|
#endif
|
|
|
|
if (!client->game)
|
|
return RC_NO_GAME_LOADED;
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
rc_client_reset_pending_events(client);
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next)
|
|
rc_client_subset_before_deserialize_progress(subset);
|
|
|
|
rc_client_hide_progress_tracker(client, client->game);
|
|
|
|
if (!serialized) {
|
|
rc_client_reset_all(client);
|
|
result = RC_OK;
|
|
}
|
|
else {
|
|
result = rc_runtime_deserialize_progress(&client->game->runtime, serialized, NULL);
|
|
}
|
|
|
|
for (subset = client->game->subsets; subset; subset = subset->next)
|
|
rc_client_subset_after_deserialize_progress(client->game, subset);
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
rc_client_raise_pending_events(client, client->game);
|
|
|
|
return result;
|
|
}
|
|
|
|
/* ===== Toggles ===== */
|
|
|
|
static void rc_client_enable_hardcore(rc_client_t* client)
|
|
{
|
|
client->state.hardcore = 1;
|
|
|
|
if (client->game) {
|
|
rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE);
|
|
rc_client_activate_leaderboards(client->game, client);
|
|
|
|
/* disable processing until the client acknowledges the reset event by calling rc_runtime_reset() */
|
|
RC_CLIENT_LOG_INFO(client, "Hardcore enabled, waiting for reset");
|
|
client->game->waiting_for_reset = 1;
|
|
}
|
|
else {
|
|
RC_CLIENT_LOG_INFO(client, "Hardcore enabled");
|
|
}
|
|
}
|
|
|
|
static void rc_client_disable_hardcore(rc_client_t* client)
|
|
{
|
|
client->state.hardcore = 0;
|
|
RC_CLIENT_LOG_INFO(client, "Hardcore disabled");
|
|
|
|
if (client->game) {
|
|
rc_client_toggle_hardcore_achievements(client->game, client, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
|
|
|
|
if (!client->state.allow_leaderboards_in_softcore)
|
|
rc_client_deactivate_leaderboards(client->game, client);
|
|
}
|
|
}
|
|
|
|
void rc_client_set_hardcore_enabled(rc_client_t* client, int enabled)
|
|
{
|
|
int changed = 0;
|
|
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_hardcore_enabled) {
|
|
client->state.external_client->set_hardcore_enabled(enabled);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
rc_mutex_lock(&client->state.mutex);
|
|
|
|
enabled = enabled ? 1 : 0;
|
|
if (client->state.hardcore != enabled) {
|
|
if (enabled)
|
|
rc_client_enable_hardcore(client);
|
|
else
|
|
rc_client_disable_hardcore(client);
|
|
|
|
changed = 1;
|
|
}
|
|
|
|
rc_mutex_unlock(&client->state.mutex);
|
|
|
|
/* events must be raised outside of lock */
|
|
if (changed && client->game) {
|
|
if (enabled) {
|
|
/* if enabling hardcore, notify client that a reset is requested */
|
|
if (client->game->waiting_for_reset) {
|
|
rc_client_event_t client_event;
|
|
memset(&client_event, 0, sizeof(client_event));
|
|
client_event.type = RC_CLIENT_EVENT_RESET;
|
|
client->callbacks.event_handler(&client_event, client);
|
|
}
|
|
}
|
|
else {
|
|
/* if disabling hardcore, leaderboards will be deactivated. raise events for hiding trackers */
|
|
rc_client_raise_pending_events(client, client->game);
|
|
}
|
|
}
|
|
}
|
|
|
|
int rc_client_get_hardcore_enabled(const rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_hardcore_enabled)
|
|
return client->state.external_client->get_hardcore_enabled();
|
|
#endif
|
|
|
|
return client->state.hardcore;
|
|
}
|
|
|
|
void rc_client_set_unofficial_enabled(rc_client_t* client, int enabled)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->set_unofficial_enabled) {
|
|
client->state.external_client->set_unofficial_enabled(enabled);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Unofficial %s", enabled ? "enabled" : "disabled");
|
|
client->state.unofficial_enabled = enabled ? 1 : 0;
|
|
}
|
|
|
|
int rc_client_get_unofficial_enabled(const rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_unofficial_enabled)
|
|
return client->state.external_client->get_unofficial_enabled();
|
|
#endif
|
|
|
|
return client->state.unofficial_enabled;
|
|
}
|
|
|
|
void rc_client_set_encore_mode_enabled(rc_client_t* client, int enabled)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->set_encore_mode_enabled) {
|
|
client->state.external_client->set_encore_mode_enabled(enabled);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Encore mode %s", enabled ? "enabled" : "disabled");
|
|
client->state.encore_mode = enabled ? 1 : 0;
|
|
}
|
|
|
|
int rc_client_get_encore_mode_enabled(const rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_encore_mode_enabled)
|
|
return client->state.external_client->get_encore_mode_enabled();
|
|
#endif
|
|
|
|
return client->state.encore_mode;
|
|
}
|
|
|
|
void rc_client_set_spectator_mode_enabled(rc_client_t* client, int enabled)
|
|
{
|
|
if (!client)
|
|
return;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->set_spectator_mode_enabled) {
|
|
client->state.external_client->set_spectator_mode_enabled(enabled);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (!enabled && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_LOCKED) {
|
|
RC_CLIENT_LOG_WARN(client, "Spectator mode cannot be disabled if it was enabled prior to loading game.");
|
|
return;
|
|
}
|
|
|
|
RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectator mode %s", enabled ? "enabled" : "disabled");
|
|
client->state.spectator_mode = enabled ? RC_CLIENT_SPECTATOR_MODE_ON : RC_CLIENT_SPECTATOR_MODE_OFF;
|
|
}
|
|
|
|
int rc_client_get_spectator_mode_enabled(const rc_client_t* client)
|
|
{
|
|
if (!client)
|
|
return 0;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client->state.external_client && client->state.external_client->get_spectator_mode_enabled)
|
|
return client->state.external_client->get_spectator_mode_enabled();
|
|
#endif
|
|
|
|
return (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) ? 0 : 1;
|
|
}
|
|
|
|
void rc_client_set_userdata(rc_client_t* client, void* userdata)
|
|
{
|
|
if (client)
|
|
client->callbacks.client_data = userdata;
|
|
}
|
|
|
|
void* rc_client_get_userdata(const rc_client_t* client)
|
|
{
|
|
return client ? client->callbacks.client_data : NULL;
|
|
}
|
|
|
|
void rc_client_set_host(const rc_client_t* client, const char* hostname)
|
|
{
|
|
/* if empty, just pass NULL */
|
|
if (hostname && !hostname[0])
|
|
hostname = NULL;
|
|
|
|
/* clear the image host so it'll use the custom host for images too */
|
|
rc_api_set_image_host(NULL);
|
|
|
|
/* set the custom host */
|
|
if (hostname && client) {
|
|
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Using host: %s", hostname);
|
|
}
|
|
rc_api_set_host(hostname);
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client && client->state.external_client && client->state.external_client->set_host)
|
|
client->state.external_client->set_host(hostname);
|
|
#endif
|
|
}
|
|
|
|
size_t rc_client_get_user_agent_clause(rc_client_t* client, char buffer[], size_t buffer_size)
|
|
{
|
|
size_t result;
|
|
|
|
#ifdef RC_CLIENT_SUPPORTS_EXTERNAL
|
|
if (client && client->state.external_client && client->state.external_client->get_user_agent_clause) {
|
|
result = client->state.external_client->get_user_agent_clause(buffer, buffer_size);
|
|
if (result > 0) {
|
|
result += snprintf(buffer + result, buffer_size - result, " rc_client/" RCHEEVOS_VERSION_STRING);
|
|
buffer[buffer_size - 1] = '\0';
|
|
return result;
|
|
}
|
|
}
|
|
#else
|
|
(void)client;
|
|
#endif
|
|
|
|
result = snprintf(buffer, buffer_size, "rcheevos/" RCHEEVOS_VERSION_STRING);
|
|
|
|
/* some implementations of snprintf will fill the buffer without null terminating.
|
|
* make sure the buffer is null terminated */
|
|
buffer[buffer_size - 1] = '\0';
|
|
return result;
|
|
}
|