dep/rcheevos: Update to ffddcdb

This commit is contained in:
Stenzek 2023-09-06 22:37:42 +10:00
parent 7d178c04d3
commit 58d62e1ab4
13 changed files with 417 additions and 168 deletions

View file

@ -26,6 +26,7 @@ add_library(rcheevos
src/rcheevos/lboard.c src/rcheevos/lboard.c
src/rcheevos/memref.c src/rcheevos/memref.c
src/rcheevos/operand.c src/rcheevos/operand.c
src/rcheevos/rc_client.c
src/rcheevos/rc_compat.h src/rcheevos/rc_compat.h
src/rcheevos/rc_internal.h src/rcheevos/rc_internal.h
src/rcheevos/richpresence.c src/rcheevos/richpresence.c

View file

@ -3,6 +3,8 @@
#include "rc_api_request.h" #include "rc_api_request.h"
#include <time.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -65,10 +67,34 @@ typedef struct rc_api_start_session_request_t {
} }
rc_api_start_session_request_t; rc_api_start_session_request_t;
/**
* Response data for an achievement unlock.
*/
typedef struct rc_api_unlock_entry_t {
/* The unique identifier of the unlocked achievement */
unsigned achievement_id;
/* When the achievement was unlocked */
time_t when;
}
rc_api_unlock_entry_t;
/** /**
* Response data for a start session request. * Response data for a start session request.
*/ */
typedef struct rc_api_start_session_response_t { typedef struct rc_api_start_session_response_t {
/* An array of hardcore user unlocks */
rc_api_unlock_entry_t* hardcore_unlocks;
/* An array of user unlocks */
rc_api_unlock_entry_t* unlocks;
/* The number of items in the hardcore_unlocks array */
unsigned num_hardcore_unlocks;
/* The number of items in the unlocks array */
unsigned num_unlocks;
/* The server timestamp when the response was generated */
time_t server_now;
/* Common server-provided response information */ /* Common server-provided response information */
rc_api_response_t response; rc_api_response_t response;
} }

View file

@ -46,11 +46,6 @@ typedef void (*rc_client_callback_t)(int result, const char* error_message, rc_c
*/ */
typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client); typedef void (*rc_client_message_callback_t)(const char* message, const rc_client_t* client);
/**
* Marks an async process as aborted. The associated callback will not be called.
*/
void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle);
/*****************************************************************************\ /*****************************************************************************\
| Runtime | | Runtime |
\*****************************************************************************/ \*****************************************************************************/
@ -130,6 +125,19 @@ void* rc_client_get_userdata(const rc_client_t* client);
*/ */
void rc_client_set_host(const rc_client_t* client, const char* hostname); void rc_client_set_host(const rc_client_t* client, const char* hostname);
typedef uint64_t rc_clock_t;
typedef rc_clock_t (*rc_get_time_millisecs_func_t)(const rc_client_t* client);
/**
* Specifies a function that returns a value that increases once per millisecond.
*/
void rc_client_set_get_time_millisecs_function(rc_client_t* client, rc_get_time_millisecs_func_t handler);
/**
* Marks an async process as aborted. The associated callback will not be called.
*/
void rc_client_abort_async(rc_client_t* client, rc_client_async_handle_t* async_handle);
/*****************************************************************************\ /*****************************************************************************\
| Logging | | Logging |
\*****************************************************************************/ \*****************************************************************************/
@ -502,7 +510,9 @@ enum {
RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */ RC_CLIENT_EVENT_LEADERBOARD_TRACKER_UPDATE = 12, /* [leaderboard_tracker] updated */
RC_CLIENT_EVENT_RESET = 13, /* emulated system should be reset (as the result of enabling hardcore) */ RC_CLIENT_EVENT_RESET = 13, /* emulated system should be reset (as the result of enabling hardcore) */
RC_CLIENT_EVENT_GAME_COMPLETED = 14, /* all achievements for the game have been earned */ RC_CLIENT_EVENT_GAME_COMPLETED = 14, /* all achievements for the game have been earned */
RC_CLIENT_EVENT_SERVER_ERROR = 15 /* an API response returned a [server_error] and will not be retried */ RC_CLIENT_EVENT_SERVER_ERROR = 15, /* an API response returned a [server_error] and will not be retried */
RC_CLIENT_EVENT_DISCONNECTED = 16, /* an unlock request could not be completed and is pending */
RC_CLIENT_EVENT_RECONNECTED = 17 /* all pending unlocks have been completed */
}; };
typedef struct rc_client_server_error_t typedef struct rc_client_server_error_t

View file

@ -57,6 +57,7 @@ enum {
RC_MEMSIZE_FLOAT, RC_MEMSIZE_FLOAT,
RC_MEMSIZE_MBF32, RC_MEMSIZE_MBF32,
RC_MEMSIZE_MBF32_LE, RC_MEMSIZE_MBF32_LE,
RC_MEMSIZE_FLOAT_BE,
RC_MEMSIZE_VARIABLE RC_MEMSIZE_VARIABLE
}; };

View file

@ -16,6 +16,7 @@
<ClCompile Include="src\rcheevos\lboard.c" /> <ClCompile Include="src\rcheevos\lboard.c" />
<ClCompile Include="src\rcheevos\memref.c" /> <ClCompile Include="src\rcheevos\memref.c" />
<ClCompile Include="src\rcheevos\operand.c" /> <ClCompile Include="src\rcheevos\operand.c" />
<ClCompile Include="src\rcheevos\rc_client.c" />
<ClCompile Include="src\rcheevos\richpresence.c" /> <ClCompile Include="src\rcheevos\richpresence.c" />
<ClCompile Include="src\rcheevos\runtime.c" /> <ClCompile Include="src\rcheevos\runtime.c" />
<ClCompile Include="src\rcheevos\runtime_progress.c" /> <ClCompile Include="src\rcheevos\runtime_progress.c" />
@ -33,6 +34,7 @@
<ClInclude Include="include\rc_api_request.h" /> <ClInclude Include="include\rc_api_request.h" />
<ClInclude Include="include\rc_api_runtime.h" /> <ClInclude Include="include\rc_api_runtime.h" />
<ClInclude Include="include\rc_api_user.h" /> <ClInclude Include="include\rc_api_user.h" />
<ClInclude Include="include\rc_client.h" />
<ClInclude Include="include\rc_consoles.h" /> <ClInclude Include="include\rc_consoles.h" />
<ClInclude Include="include\rc_error.h" /> <ClInclude Include="include\rc_error.h" />
<ClInclude Include="include\rc_hash.h" /> <ClInclude Include="include\rc_hash.h" />
@ -40,6 +42,7 @@
<ClInclude Include="include\rc_runtime_types.h" /> <ClInclude Include="include\rc_runtime_types.h" />
<ClInclude Include="include\rc_url.h" /> <ClInclude Include="include\rc_url.h" />
<ClInclude Include="src\rapi\rc_api_common.h" /> <ClInclude Include="src\rapi\rc_api_common.h" />
<ClInclude Include="src\rcheevos\rc_client_internal.h" />
<ClInclude Include="src\rcheevos\rc_compat.h" /> <ClInclude Include="src\rcheevos\rc_compat.h" />
<ClInclude Include="src\rcheevos\rc_internal.h" /> <ClInclude Include="src\rcheevos\rc_internal.h" />
<ClInclude Include="src\rhash\md5.h" /> <ClInclude Include="src\rhash\md5.h" />

View file

@ -87,6 +87,9 @@
<ClCompile Include="src\rapi\rc_api_common.c"> <ClCompile Include="src\rapi\rc_api_common.c">
<Filter>rapi</Filter> <Filter>rapi</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="src\rcheevos\rc_client.c">
<Filter>rcheevos</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="include\rc_consoles.h"> <ClInclude Include="include\rc_consoles.h">
@ -137,5 +140,11 @@
<ClInclude Include="src\rapi\rc_api_common.h"> <ClInclude Include="src\rapi\rc_api_common.h">
<Filter>rapi</Filter> <Filter>rapi</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="src\rcheevos\rc_client_internal.h">
<Filter>rcheevos</Filter>
</ClInclude>
<ClInclude Include="include\rc_client.h">
<Filter>include</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -419,6 +419,18 @@ int rc_json_get_required_unum_array(unsigned** entries, unsigned* num_entries, r
} }
int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) { int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
#ifndef NDEBUG
if (strcmp(field->name, field_name) != 0)
return 0;
#endif
if (!rc_json_get_optional_array(num_entries, array_field, response, field, field_name))
return rc_json_missing_field(response, field);
return 1;
}
int rc_json_get_optional_array(unsigned* num_entries, rc_json_field_t* array_field, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name) {
#ifndef NDEBUG #ifndef NDEBUG
if (strcmp(field->name, field_name) != 0) if (strcmp(field->name, field_name) != 0)
return 0; return 0;
@ -428,7 +440,7 @@ int rc_json_get_required_array(unsigned* num_entries, rc_json_field_t* array_fie
if (!field->value_start || *field->value_start != '[') { if (!field->value_start || *field->value_start != '[') {
*num_entries = 0; *num_entries = 0;
return rc_json_missing_field(response, field); return 0;
} }
memcpy(array_field, field, sizeof(*array_field)); memcpy(array_field, field, sizeof(*array_field));

View file

@ -53,6 +53,7 @@ void rc_json_get_optional_string(const char** out, rc_api_response_t* response,
void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value); void rc_json_get_optional_num(int* out, const rc_json_field_t* field, const char* field_name, int default_value);
void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value); void rc_json_get_optional_unum(unsigned* out, const rc_json_field_t* field, const char* field_name, unsigned default_value);
void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value); void rc_json_get_optional_bool(int* out, const rc_json_field_t* field, const char* field_name, int default_value);
int rc_json_get_optional_array(unsigned* num_entries, rc_json_field_t* iterator, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name);
int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_string(const char** out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name);
int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_num(int* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name);
int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name); int rc_json_get_required_unum(unsigned* out, rc_api_response_t* response, const rc_json_field_t* field, const char* field_name);

View file

@ -91,16 +91,8 @@ int rc_api_init_start_session_request(rc_api_request_t* request, const rc_api_st
return RC_INVALID_STATE; return RC_INVALID_STATE;
rc_url_builder_init(&builder, &request->buffer, 48); rc_url_builder_init(&builder, &request->buffer, 48);
if (rc_api_url_build_dorequest(&builder, "postactivity", api_params->username, api_params->api_token)) { if (rc_api_url_build_dorequest(&builder, "startsession", api_params->username, api_params->api_token)) {
/* activity type enum (only 3 is used ) rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
* 1 = earned achievement - handled by awardachievement
* 2 = logged in - handled by login
* 3 = started playing
* 4 = uploaded achievement - handled by uploadachievement
* 5 = modified achievement - handled by uploadachievement
*/
rc_url_builder_append_unum_param(&builder, "a", 3);
rc_url_builder_append_unum_param(&builder, "m", api_params->game_id);
rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING); rc_url_builder_append_str_param(&builder, "l", RCHEEVOS_VERSION_STRING);
request->post_data = rc_url_builder_finalize(&builder); request->post_data = rc_url_builder_finalize(&builder);
request->content_type = RC_CONTENT_TYPE_URLENCODED; request->content_type = RC_CONTENT_TYPE_URLENCODED;
@ -120,15 +112,78 @@ int rc_api_process_start_session_response(rc_api_start_session_response_t* respo
} }
int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) { int rc_api_process_start_session_server_response(rc_api_start_session_response_t* response, const rc_api_server_response_t* server_response) {
rc_api_unlock_entry_t* unlock;
rc_json_field_t array_field;
rc_json_iterator_t iterator;
unsigned timet;
int result;
rc_json_field_t fields[] = { rc_json_field_t fields[] = {
RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Success"),
RC_JSON_NEW_FIELD("Error") RC_JSON_NEW_FIELD("Error"),
RC_JSON_NEW_FIELD("Unlocks"),
RC_JSON_NEW_FIELD("HardcoreUnlocks"),
RC_JSON_NEW_FIELD("ServerNow")
};
rc_json_field_t unlock_entry_fields[] = {
RC_JSON_NEW_FIELD("ID"),
RC_JSON_NEW_FIELD("When")
}; };
memset(response, 0, sizeof(*response)); memset(response, 0, sizeof(*response));
rc_buf_init(&response->response.buffer); rc_buf_init(&response->response.buffer);
return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0])); result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
if (result != RC_OK || !response->response.succeeded)
return result;
if (rc_json_get_optional_array(&response->num_unlocks, &array_field, &response->response, &fields[2], "Unlocks") && response->num_unlocks) {
response->unlocks = (rc_api_unlock_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_unlocks * sizeof(rc_api_unlock_entry_t));
if (!response->unlocks)
return RC_OUT_OF_MEMORY;
memset(&iterator, 0, sizeof(iterator));
iterator.json = array_field.value_start;
iterator.end = array_field.value_end;
unlock = response->unlocks;
while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) {
if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID"))
return RC_MISSING_VALUE;
if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When"))
return RC_MISSING_VALUE;
unlock->when = (time_t)timet;
++unlock;
}
}
if (rc_json_get_optional_array(&response->num_hardcore_unlocks, &array_field, &response->response, &fields[3], "HardcoreUnlocks") && response->num_hardcore_unlocks) {
response->hardcore_unlocks = (rc_api_unlock_entry_t*)rc_buf_alloc(&response->response.buffer, response->num_hardcore_unlocks * sizeof(rc_api_unlock_entry_t));
if (!response->hardcore_unlocks)
return RC_OUT_OF_MEMORY;
memset(&iterator, 0, sizeof(iterator));
iterator.json = array_field.value_start;
iterator.end = array_field.value_end;
unlock = response->hardcore_unlocks;
while (rc_json_get_array_entry_object(unlock_entry_fields, sizeof(unlock_entry_fields) / sizeof(unlock_entry_fields[0]), &iterator)) {
if (!rc_json_get_required_unum(&unlock->achievement_id, &response->response, &unlock_entry_fields[0], "ID"))
return RC_MISSING_VALUE;
if (!rc_json_get_required_unum(&timet, &response->response, &unlock_entry_fields[1], "When"))
return RC_MISSING_VALUE;
unlock->when = (time_t)timet;
++unlock;
}
}
rc_json_get_optional_unum(&timet, &fields[4], "ServerNow", 0);
response->server_now = (time_t)timet;
return RC_OK;
} }
void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) { void rc_api_destroy_start_session_response(rc_api_start_session_response_t* response) {

View file

@ -94,6 +94,7 @@ int rc_parse_memref(const char** memaddr, char* size, unsigned* address) {
++aux; ++aux;
switch (*aux++) { switch (*aux++) {
case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break; case 'f': case 'F': *size = RC_MEMSIZE_FLOAT; break;
case 'b': case 'B': *size = RC_MEMSIZE_FLOAT_BE; break;
case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break; case 'm': case 'M': *size = RC_MEMSIZE_MBF32; break;
case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break; case 'l': case 'L': *size = RC_MEMSIZE_MBF32_LE; break;
@ -185,6 +186,18 @@ static void rc_transform_memref_float(rc_typed_value_t* value) {
value->type = RC_VALUE_TYPE_FLOAT; value->type = RC_VALUE_TYPE_FLOAT;
} }
static void rc_transform_memref_float_be(rc_typed_value_t* value) {
/* decodes an IEEE 754 float in big endian format */
const unsigned mantissa = ((value->value.u32 & 0xFF000000) >> 24) |
((value->value.u32 & 0x00FF0000) >> 8) |
((value->value.u32 & 0x00007F00) << 8);
const int exponent = (int)(((value->value.u32 & 0x0000007F) << 1) |
((value->value.u32 & 0x00008000) >> 15)) - 127;
const int sign = (value->value.u32 & 0x00000080);
value->value.f32 = rc_build_float(mantissa, exponent, sign);
value->type = RC_VALUE_TYPE_FLOAT;
}
static void rc_transform_memref_mbf32(rc_typed_value_t* value) { static void rc_transform_memref_mbf32(rc_typed_value_t* value) {
/* decodes a Microsoft Binary Format float */ /* decodes a Microsoft Binary Format float */
/* NOTE: 32-bit MBF is stored in memory as big endian (at least for Apple II) */ /* NOTE: 32-bit MBF is stored in memory as big endian (at least for Apple II) */
@ -305,6 +318,10 @@ void rc_transform_memref_value(rc_typed_value_t* value, char size) {
rc_transform_memref_float(value); rc_transform_memref_float(value);
break; break;
case RC_MEMSIZE_FLOAT_BE:
rc_transform_memref_float_be(value);
break;
case RC_MEMSIZE_MBF32: case RC_MEMSIZE_MBF32:
rc_transform_memref_mbf32(value); rc_transform_memref_mbf32(value);
break; break;
@ -340,6 +357,7 @@ static const unsigned rc_memref_masks[] = {
0xffffffff, /* RC_MEMSIZE_FLOAT */ 0xffffffff, /* RC_MEMSIZE_FLOAT */
0xffffffff, /* RC_MEMSIZE_MBF32 */ 0xffffffff, /* RC_MEMSIZE_MBF32 */
0xffffffff, /* RC_MEMSIZE_MBF32_LE */ 0xffffffff, /* RC_MEMSIZE_MBF32_LE */
0xffffffff, /* RC_MEMSIZE_FLOAT_BE */
0xffffffff /* RC_MEMSIZE_VARIABLE */ 0xffffffff /* RC_MEMSIZE_VARIABLE */
}; };
@ -376,6 +394,7 @@ static const char rc_memref_shared_sizes[] = {
RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT */
RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32 */
RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */ RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_MBF32_LE */
RC_MEMSIZE_32_BITS, /* RC_MEMSIZE_FLOAT_BE */
RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */ RC_MEMSIZE_32_BITS /* RC_MEMSIZE_VARIABLE */
}; };

View file

@ -301,6 +301,7 @@ static int rc_luapeek(lua_State* L) {
int rc_operand_is_float_memref(const rc_operand_t* self) { int rc_operand_is_float_memref(const rc_operand_t* self) {
switch (self->size) { switch (self->size) {
case RC_MEMSIZE_FLOAT: case RC_MEMSIZE_FLOAT:
case RC_MEMSIZE_FLOAT_BE:
case RC_MEMSIZE_MBF32: case RC_MEMSIZE_MBF32:
case RC_MEMSIZE_MBF32_LE: case RC_MEMSIZE_MBF32_LE:
return 1; return 1;

View file

@ -11,14 +11,17 @@
#include <stdarg.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_UNKNOWN_GAME_ID (uint32_t)-1
#define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */ #define RC_CLIENT_RECENT_UNLOCK_DELAY_SECONDS (10 * 60) /* ten minutes */
/* clock_t can wrap. For most cases, we won't have to worry about it because it won't
* overflow until after over a month of runtime. But some cases can overflow in as short as
* 36 minutes. Use substraction as a secondary check to ensure an overflow hasn't occurred. */
#define RC_CLIENT_CLOCK_IS_BEFORE(clk, cmp_clk) (clk < cmp_clk && (cmp_clk - clk) > 0)
struct rc_client_async_handle_t { struct rc_client_async_handle_t {
uint8_t aborted; uint8_t aborted;
}; };
@ -52,10 +55,7 @@ typedef struct rc_client_load_state_t
rc_hash_iterator_t hash_iterator; rc_hash_iterator_t hash_iterator;
rc_client_pending_media_t* pending_media; rc_client_pending_media_t* pending_media;
uint32_t* hardcore_unlocks; rc_api_start_session_response_t *start_session_response;
uint32_t* softcore_unlocks;
uint32_t num_hardcore_unlocks;
uint32_t num_softcore_unlocks;
rc_client_async_handle_t async_handle; rc_client_async_handle_t async_handle;
@ -68,11 +68,13 @@ static void rc_client_begin_fetch_game_data(rc_client_load_state_t* callback_dat
static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_info_t* game); 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 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 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, clock_t now); 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_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_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game);
static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard); static void rc_client_release_leaderboard_tracker(rc_client_game_info_t* game, rc_client_leaderboard_info_t* leaderboard);
static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, clock_t when); 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 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 ===== */ /* ===== Construction/Destruction ===== */
@ -92,6 +94,7 @@ rc_client_t* rc_client_create(rc_client_read_memory_func_t read_memory_function,
client->callbacks.server_call = server_call_function; client->callbacks.server_call = server_call_function;
client->callbacks.event_handler = rc_client_dummy_event_handler; client->callbacks.event_handler = rc_client_dummy_event_handler;
rc_client_set_legacy_peek(client, RC_CLIENT_LEGACY_PEEK_AUTO); 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_mutex_init(&client->state.mutex);
@ -207,6 +210,68 @@ void rc_client_enable_logging(rc_client_t* client, int level, rc_client_message_
/* ===== Common ===== */ /* ===== Common ===== */
static rc_clock_t rc_client_clock_get_now_millisecs(const rc_client_t* client)
{
#if defined(CLOCK_MONOTONIC)
struct timespec now;
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;
/* 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();
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;
}
static int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle) static int rc_client_async_handle_aborted(rc_client_t* client, rc_client_async_handle_t* async_handle)
{ {
int aborted; int aborted;
@ -261,6 +326,56 @@ static void rc_client_raise_server_error_event(rc_client_t* client, const char*
client->callbacks.event_handler(&client_event, client); 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) static int rc_client_should_retry(const rc_api_server_response_t* server_response)
{ {
switch (server_response->http_status_code) { switch (server_response->http_status_code) {
@ -276,6 +391,22 @@ static int rc_client_should_retry(const rc_api_server_response_t* server_respons
/* too many unlocks occurred at the same time */ /* too many unlocks occurred at the same time */
return 1; 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;
default: default:
return 0; return 0;
} }
@ -530,7 +661,7 @@ static void rc_client_subset_get_user_game_summary(const rc_client_subset_info_t
++summary->num_unlocked_achievements; ++summary->num_unlocked_achievements;
summary->points_unlocked += achievement->public_.points; summary->points_unlocked += achievement->public_.points;
} }
else if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) { if (achievement->public_.bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNSUPPORTED) {
++summary->num_unsupported_achievements; ++summary->num_unsupported_achievements;
} }
@ -581,10 +712,10 @@ static void rc_client_free_load_state(rc_client_load_state_t* load_state)
if (load_state->game) if (load_state->game)
rc_client_free_game(load_state->game); rc_client_free_game(load_state->game);
if (load_state->hardcore_unlocks) if (load_state->start_session_response) {
free(load_state->hardcore_unlocks); rc_api_destroy_start_session_response(load_state->start_session_response);
if (load_state->softcore_unlocks) free(load_state->start_session_response);
free(load_state->softcore_unlocks); }
free(load_state); free(load_state);
} }
@ -1001,19 +1132,24 @@ static void rc_client_deactivate_leaderboards(rc_client_game_info_t* game, rc_cl
game->runtime.lboard_count = 0; game->runtime.lboard_count = 0;
} }
static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, uint32_t* unlocks, uint32_t num_unlocks, uint8_t mode) 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* start = subset->achievements;
rc_client_achievement_info_t* stop = start + subset->public_.num_achievements; rc_client_achievement_info_t* stop = start + subset->public_.num_achievements;
rc_client_achievement_info_t* scan; rc_client_achievement_info_t* scan;
unsigned i; rc_api_unlock_entry_t* unlock = unlocks;
rc_api_unlock_entry_t* unlock_stop = unlocks + num_unlocks;
for (i = 0; i < num_unlocks; ++i) { for (; unlock < unlock_stop; ++unlock) {
uint32_t id = unlocks[i];
for (scan = start; scan < stop; ++scan) { for (scan = start; scan < stop; ++scan) {
if (scan->public_.id == id) { if (scan->public_.id == unlock->achievement_id) {
scan->public_.unlocked |= mode; 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) if (scan == start)
++start; ++start;
else if (scan + 1 == stop) else if (scan + 1 == stop)
@ -1024,7 +1160,7 @@ static void rc_client_apply_unlocks(rc_client_subset_info_t* subset, uint32_t* u
} }
} }
static void rc_client_activate_game(rc_client_load_state_t* load_state) 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_client_t* client = load_state->client;
@ -1039,18 +1175,17 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state)
if (load_state->callback) if (load_state->callback)
load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata); load_state->callback(RC_ABORTED, "The requested game is no longer active", client, load_state->callback_userdata);
} }
else if ((!load_state->softcore_unlocks || !load_state->hardcore_unlocks) && else if (!start_session_response && client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
/* unlocks not available - assume malloc failed */ /* unlocks not available - assume malloc failed */
if (load_state->callback) if (load_state->callback)
load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata); load_state->callback(RC_INVALID_STATE, "Unlock arrays were not allocated", client, load_state->callback_userdata);
} }
else { else {
if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) { if (client->state.spectator_mode == RC_CLIENT_SPECTATOR_MODE_OFF) {
rc_client_apply_unlocks(load_state->subset, load_state->softcore_unlocks, rc_client_apply_unlocks(load_state->subset, start_session_response->hardcore_unlocks,
load_state->num_softcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE); start_session_response->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH);
rc_client_apply_unlocks(load_state->subset, load_state->hardcore_unlocks, rc_client_apply_unlocks(load_state->subset, start_session_response->unlocks,
load_state->num_hardcore_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_BOTH); start_session_response->num_unlocks, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
} }
rc_mutex_lock(&client->state.mutex); rc_mutex_lock(&client->state.mutex);
@ -1094,11 +1229,11 @@ static void rc_client_activate_game(rc_client_load_state_t* load_state)
memset(callback_data, 0, sizeof(*callback_data)); memset(callback_data, 0, sizeof(*callback_data));
callback_data->callback = rc_client_ping; callback_data->callback = rc_client_ping;
callback_data->related_id = load_state->game->public_.id; callback_data->related_id = load_state->game->public_.id;
callback_data->when = clock() + 30 * CLOCKS_PER_SEC; callback_data->when = client->callbacks.get_time_millisecs(client) + 30 * 1000;
rc_client_schedule_callback(client, callback_data); rc_client_schedule_callback(client, callback_data);
} }
RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcode %s%s", load_state->game->public_.id, RC_CLIENT_LOG_INFO_FORMATTED(client, "Game %u loaded, hardcore %s%s", load_state->game->public_.id,
client->state.hardcore ? "enabled" : "disabled", client->state.hardcore ? "enabled" : "disabled",
(client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : ""); (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) ? ", spectating" : "");
} }
@ -1142,80 +1277,30 @@ static void rc_client_start_session_callback(const rc_api_server_response_t* ser
else if (outstanding_requests < 0) { else if (outstanding_requests < 0) {
/* previous load state was aborted, load_state was free'd */ /* 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 { else {
if (outstanding_requests == 0) load_state->start_session_response =
rc_client_activate_game(load_state); (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); rc_api_destroy_start_session_response(&start_session_response);
} }
static void rc_client_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data, int mode)
{
rc_client_load_state_t* load_state = (rc_client_load_state_t*)callback_data;
rc_api_fetch_user_unlocks_response_t fetch_user_unlocks_response;
int outstanding_requests;
const char* error_message;
int result;
if (rc_client_async_handle_aborted(load_state->client, &load_state->async_handle)) {
rc_client_t* client = load_state->client;
rc_client_load_aborted(load_state);
RC_CLIENT_LOG_VERBOSE(client, "Load aborted while fetching unlocks");
return;
}
result = rc_api_process_fetch_user_unlocks_server_response(&fetch_user_unlocks_response, server_response);
error_message = rc_client_server_error_message(&result, server_response->http_status_code, &fetch_user_unlocks_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 (mode == RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE) {
const size_t array_size = fetch_user_unlocks_response.num_achievement_ids * sizeof(uint32_t);
load_state->num_hardcore_unlocks = fetch_user_unlocks_response.num_achievement_ids;
load_state->hardcore_unlocks = (uint32_t*)malloc(array_size);
if (load_state->hardcore_unlocks)
memcpy(load_state->hardcore_unlocks, fetch_user_unlocks_response.achievement_ids, array_size);
}
else {
const size_t array_size = fetch_user_unlocks_response.num_achievement_ids * sizeof(uint32_t);
load_state->num_softcore_unlocks = fetch_user_unlocks_response.num_achievement_ids;
load_state->softcore_unlocks = (uint32_t*)malloc(array_size);
if (load_state->softcore_unlocks)
memcpy(load_state->softcore_unlocks, fetch_user_unlocks_response.achievement_ids, array_size);
}
if (outstanding_requests == 0)
rc_client_activate_game(load_state);
}
rc_api_destroy_fetch_user_unlocks_response(&fetch_user_unlocks_response);
}
static void rc_client_hardcore_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data)
{
rc_client_unlocks_callback(server_response, callback_data, RC_CLIENT_ACHIEVEMENT_UNLOCKED_HARDCORE);
}
static void rc_client_softcore_unlocks_callback(const rc_api_server_response_t* server_response, void* callback_data)
{
rc_client_unlocks_callback(server_response, callback_data, RC_CLIENT_ACHIEVEMENT_UNLOCKED_SOFTCORE);
}
static void rc_client_begin_start_session(rc_client_load_state_t* load_state) static void rc_client_begin_start_session(rc_client_load_state_t* load_state)
{ {
rc_api_start_session_request_t start_session_params; rc_api_start_session_request_t start_session_params;
rc_api_fetch_user_unlocks_request_t unlock_params;
rc_client_t* client = load_state->client; rc_client_t* client = load_state->client;
rc_api_request_t start_session_request; rc_api_request_t start_session_request;
rc_api_request_t hardcore_unlock_request;
rc_api_request_t softcore_unlock_request;
int result; int result;
memset(&start_session_params, 0, sizeof(start_session_params)); memset(&start_session_params, 0, sizeof(start_session_params));
@ -1228,38 +1313,9 @@ static void rc_client_begin_start_session(rc_client_load_state_t* load_state)
rc_client_load_error(load_state, result, rc_error_str(result)); rc_client_load_error(load_state, result, rc_error_str(result));
} }
else { else {
memset(&unlock_params, 0, sizeof(unlock_params)); rc_client_begin_load_state(load_state, RC_CLIENT_LOAD_STATE_STARTING_SESSION, 1);
unlock_params.username = client->user.username;
unlock_params.api_token = client->user.token;
unlock_params.game_id = load_state->hash->game_id;
unlock_params.hardcore = 1;
result = rc_api_init_fetch_user_unlocks_request(&hardcore_unlock_request, &unlock_params);
if (result != RC_OK) {
rc_client_load_error(load_state, result, rc_error_str(result));
}
else {
unlock_params.hardcore = 0;
result = rc_api_init_fetch_user_unlocks_request(&softcore_unlock_request, &unlock_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_STATE_STARTING_SESSION, 3);
/* TODO: create single server request to do all three of these */
RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id); RC_CLIENT_LOG_VERBOSE_FORMATTED(client, "Starting session for game %u", start_session_params.game_id);
client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client); client->callbacks.server_call(&start_session_request, rc_client_start_session_callback, load_state, client);
client->callbacks.server_call(&hardcore_unlock_request, rc_client_hardcore_unlocks_callback, load_state, client);
client->callbacks.server_call(&softcore_unlock_request, rc_client_softcore_unlocks_callback, load_state, client);
rc_api_destroy_request(&softcore_unlock_request);
}
rc_api_destroy_request(&hardcore_unlock_request);
}
rc_api_destroy_request(&start_session_request); rc_api_destroy_request(&start_session_request);
} }
} }
@ -1590,13 +1646,18 @@ static void rc_client_fetch_game_data_callback(const rc_api_server_response_t* s
scan->next = subset; 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); outstanding_requests = rc_client_end_load_state(load_state);
if (outstanding_requests < 0) { if (outstanding_requests < 0) {
/* previous load state was aborted, load_state was free'd */ /* previous load state was aborted, load_state was free'd */
} }
else { else {
if (outstanding_requests == 0) if (outstanding_requests == 0)
rc_client_activate_game(load_state); rc_client_activate_game(load_state, load_state->start_session_response);
} }
} }
@ -2299,7 +2360,7 @@ void rc_client_begin_load_subset(rc_client_t* client, uint32_t subset_id, rc_cli
return; return;
} }
snprintf(buffer, sizeof(buffer), "[SUBSET%u]", subset_id); snprintf(buffer, sizeof(buffer), "[SUBSET%lu]", (unsigned long)subset_id);
load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state)); load_state = (rc_client_load_state_t*)calloc(1, sizeof(*load_state));
if (!load_state) { if (!load_state) {
@ -2372,11 +2433,11 @@ static void rc_client_update_achievement_display_information(rc_client_t* client
if (!achievement->trigger->measured_as_percent) { if (!achievement->trigger->measured_as_percent) {
snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress),
"%u/%u", new_measured_value, achievement->trigger->measured_target); "%lu/%lu", (unsigned long)new_measured_value, (unsigned long)achievement->trigger->measured_target);
} }
else if (achievement->public_.measured_percent >= 1.0) { else if (achievement->public_.measured_percent >= 1.0) {
snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress), snprintf(achievement->public_.measured_progress, sizeof(achievement->public_.measured_progress),
"%u%%", (uint32_t)achievement->public_.measured_percent); "%lu%%", (unsigned long)achievement->public_.measured_percent);
} }
} }
} }
@ -2704,7 +2765,7 @@ typedef struct rc_client_award_achievement_callback_data_t
static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); 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, clock_t now) 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* ach_data =
(rc_client_award_achievement_callback_data_t*)callback_data->data; (rc_client_award_achievement_callback_data_t*)callback_data->data;
@ -2736,7 +2797,7 @@ static void rc_client_award_achievement_callback(const rc_api_server_response_t*
else { else {
/* double wait time between each attempt until we hit a maximum delay of two minutes */ /* 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 ...*/ /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/
const uint32_t delay = (ach_data->retry_count > 7) ? 120 : (1 << (ach_data->retry_count - 1)); 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); 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) { if (!ach_data->scheduled_callback_data) {
@ -2751,9 +2812,12 @@ static void rc_client_award_achievement_callback(const rc_api_server_response_t*
ach_data->scheduled_callback_data->related_id = ach_data->id; ach_data->scheduled_callback_data->related_id = ach_data->id;
} }
ach_data->scheduled_callback_data->when = clock() + delay * CLOCKS_PER_SEC; 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_schedule_callback(ach_data->client, ach_data->scheduled_callback_data);
rc_client_update_disconnect_state(ach_data->client);
return; return;
} }
} }
@ -2803,6 +2867,9 @@ static void rc_client_award_achievement_callback(const rc_api_server_response_t*
} }
} }
if (ach_data->retry_count)
rc_client_update_disconnect_state(ach_data->client);
if (ach_data->scheduled_callback_data) if (ach_data->scheduled_callback_data)
free(ach_data->scheduled_callback_data); free(ach_data->scheduled_callback_data);
free(ach_data); free(ach_data);
@ -2860,6 +2927,12 @@ static void rc_client_award_achievement(rc_client_t* client, rc_client_achieveme
rc_mutex_unlock(&client->state.mutex); 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 */ /* can't unlock unofficial achievements on the server */
if (achievement->public_.category != RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE) { 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); RC_CLIENT_LOG_INFO_FORMATTED(client, "Unlocked unofficial achievement %u: %s", achievement->public_.id, achievement->public_.title);
@ -3264,7 +3337,7 @@ typedef struct 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_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, clock_t now) 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* lboard_data =
(rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data; (rc_client_submit_leaderboard_entry_callback_data_t*)callback_data->data;
@ -3296,7 +3369,7 @@ static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_resp
else { else {
/* double wait time between each attempt until we hit a maximum delay of two minutes */ /* 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 ...*/ /* 1s -> 2s -> 4s -> 8s -> 16s -> 32s -> 64s -> 120s -> 120s -> 120s ...*/
const uint32_t delay = (lboard_data->retry_count > 7) ? 120 : (1 << (lboard_data->retry_count - 1)); 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); 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) { if (!lboard_data->scheduled_callback_data) {
@ -3311,9 +3384,12 @@ static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_resp
lboard_data->scheduled_callback_data->related_id = lboard_data->id; lboard_data->scheduled_callback_data->related_id = lboard_data->id;
} }
lboard_data->scheduled_callback_data->when = clock() + delay * CLOCKS_PER_SEC; 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_schedule_callback(lboard_data->client, lboard_data->scheduled_callback_data);
rc_client_update_disconnect_state(lboard_data->client);
return; return;
} }
} }
@ -3327,6 +3403,9 @@ static void rc_client_submit_leaderboard_entry_callback(const rc_api_server_resp
} }
} }
if (lboard_data->retry_count)
rc_client_update_disconnect_state(lboard_data->client);
if (lboard_data->scheduled_callback_data) if (lboard_data->scheduled_callback_data)
free(lboard_data->scheduled_callback_data); free(lboard_data->scheduled_callback_data);
free(lboard_data); free(lboard_data);
@ -3360,6 +3439,12 @@ static void rc_client_submit_leaderboard_entry(rc_client_t* client, rc_client_le
{ {
rc_client_submit_leaderboard_entry_callback_data_t* callback_data; rc_client_submit_leaderboard_entry_callback_data_t* callback_data;
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 */ /* don't actually submit leaderboard entries when spectating */
if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) { if (client->state.spectator_mode != RC_CLIENT_SPECTATOR_MODE_OFF) {
RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s", RC_CLIENT_LOG_INFO_FORMATTED(client, "Spectated %s (%d) for leaderboard %u: %s",
@ -3587,7 +3672,7 @@ static void rc_client_ping_callback(const rc_api_server_response_t* server_respo
rc_api_destroy_ping_response(&response); rc_api_destroy_ping_response(&response);
} }
static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) 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_ping_request_t api_params;
rc_api_request_t request; rc_api_request_t request;
@ -3611,7 +3696,7 @@ static void rc_client_ping(rc_client_scheduled_callback_data_t* callback_data, r
client->callbacks.server_call(&request, rc_client_ping_callback, client, client); client->callbacks.server_call(&request, rc_client_ping_callback, client, client);
} }
callback_data->when = now + 120 * CLOCKS_PER_SEC; callback_data->when = now + 120 * 1000;
rc_client_schedule_callback(client, callback_data); rc_client_schedule_callback(client, callback_data);
} }
@ -3738,8 +3823,12 @@ static unsigned rc_client_peek(unsigned address, unsigned num_bytes, void* ud)
void rc_client_set_legacy_peek(rc_client_t* client, int method) void rc_client_set_legacy_peek(rc_client_t* client, int method)
{ {
if (method == RC_CLIENT_LEGACY_PEEK_AUTO) { if (method == RC_CLIENT_LEGACY_PEEK_AUTO) {
uint8_t buffer[4] = { 1,0,0,0 }; union {
method = (*((uint32_t*)buffer) == 1) ? 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; RC_CLIENT_LEGACY_PEEK_LITTLE_ENDIAN_READS : RC_CLIENT_LEGACY_PEEK_CONSTRUCTED;
} }
@ -3860,7 +3949,7 @@ static void rc_client_hide_progress_tracker(rc_client_t* client, rc_client_game_
} }
} }
static void rc_client_progress_tracker_timer_elapsed(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now) 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; rc_client_event_t client_event;
memset(&client_event, 0, sizeof(client_event)); memset(&client_event, 0, sizeof(client_event));
@ -3890,7 +3979,8 @@ static void rc_client_do_frame_update_progress_tracker(rc_client_t* client, rc_c
else else
game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE; game->progress_tracker.action = RC_CLIENT_PROGRESS_TRACKER_ACTION_UPDATE;
rc_client_reschedule_callback(client, game->progress_tracker.hide_callback, clock() + 2 * CLOCKS_PER_SEC); 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) static void rc_client_raise_progress_tracker_events(rc_client_t* client, rc_client_game_info_t* game)
@ -4216,13 +4306,13 @@ void rc_client_idle(rc_client_t* client)
scheduled_callback = client->state.scheduled_callbacks; scheduled_callback = client->state.scheduled_callbacks;
if (scheduled_callback) { if (scheduled_callback) {
const clock_t now = clock(); const rc_clock_t now = client->callbacks.get_time_millisecs(client);
do { do {
rc_mutex_lock(&client->state.mutex); rc_mutex_lock(&client->state.mutex);
scheduled_callback = client->state.scheduled_callbacks; scheduled_callback = client->state.scheduled_callbacks;
if (scheduled_callback) { if (scheduled_callback) {
if (RC_CLIENT_CLOCK_IS_BEFORE(now, scheduled_callback->when)) { if (scheduled_callback->when > now) {
/* not time for next callback yet, ignore it */ /* not time for next callback yet, ignore it */
scheduled_callback = NULL; scheduled_callback = NULL;
} }
@ -4239,6 +4329,9 @@ void rc_client_idle(rc_client_t* client)
scheduled_callback->callback(scheduled_callback, client, now); scheduled_callback->callback(scheduled_callback, client, now);
} while (1); } 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) void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* scheduled_callback)
@ -4251,7 +4344,7 @@ void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callba
last = &client->state.scheduled_callbacks; last = &client->state.scheduled_callbacks;
do { do {
next = *last; next = *last;
if (!next || RC_CLIENT_CLOCK_IS_BEFORE(scheduled_callback->when, next->when)) { if (!next || scheduled_callback->when < next->when) {
scheduled_callback->next = next; scheduled_callback->next = next;
*last = scheduled_callback; *last = scheduled_callback;
break; break;
@ -4264,7 +4357,7 @@ void rc_client_schedule_callback(rc_client_t* client, rc_client_scheduled_callba
} }
static void rc_client_reschedule_callback(rc_client_t* client, static void rc_client_reschedule_callback(rc_client_t* client,
rc_client_scheduled_callback_data_t* callback, clock_t when) rc_client_scheduled_callback_data_t* callback, rc_clock_t when)
{ {
rc_client_scheduled_callback_data_t** last; rc_client_scheduled_callback_data_t** last;
rc_client_scheduled_callback_data_t* next; rc_client_scheduled_callback_data_t* next;
@ -4290,7 +4383,7 @@ static void rc_client_reschedule_callback(rc_client_t* client,
break; break;
} }
if (RC_CLIENT_CLOCK_IS_BEFORE(when, next->next->when)) { if (when < next->next->when) {
/* already in the correct place */ /* already in the correct place */
break; break;
} }
@ -4301,7 +4394,7 @@ static void rc_client_reschedule_callback(rc_client_t* client,
continue; continue;
} }
if (!next || RC_CLIENT_CLOCK_IS_BEFORE(when, next->when)) { if (!next || when < next->when) {
/* insert here */ /* insert here */
callback->next = next; callback->next = next;
*last = callback; *last = callback;

View file

@ -11,21 +11,31 @@ extern "C" {
#include "rc_runtime.h" #include "rc_runtime.h"
#include "rc_runtime_types.h" #include "rc_runtime_types.h"
struct rc_api_fetch_game_data_response_t;
typedef void (*rc_client_post_process_game_data_response_t)(const rc_api_server_response_t* server_response,
struct rc_api_fetch_game_data_response_t* game_data_response, rc_client_t* client, void* userdata);
typedef int (*rc_client_can_submit_achievement_unlock_t)(uint32_t achievement_id, rc_client_t* client);
typedef int (*rc_client_can_submit_leaderboard_entry_t)(uint32_t leaderboard_id, rc_client_t* client);
typedef struct rc_client_callbacks_t { typedef struct rc_client_callbacks_t {
rc_client_read_memory_func_t read_memory; rc_client_read_memory_func_t read_memory;
rc_client_event_handler_t event_handler; rc_client_event_handler_t event_handler;
rc_client_server_call_t server_call; rc_client_server_call_t server_call;
rc_client_message_callback_t log_call; rc_client_message_callback_t log_call;
rc_get_time_millisecs_func_t get_time_millisecs;
rc_client_post_process_game_data_response_t post_process_game_data_response;
rc_client_can_submit_achievement_unlock_t can_submit_achievement_unlock;
rc_client_can_submit_leaderboard_entry_t can_submit_leaderboard_entry;
void* client_data; void* client_data;
} rc_client_callbacks_t; } rc_client_callbacks_t;
struct rc_client_scheduled_callback_data_t; struct rc_client_scheduled_callback_data_t;
typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, clock_t now); typedef void (*rc_client_scheduled_callback_t)(struct rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now);
typedef struct rc_client_scheduled_callback_data_t typedef struct rc_client_scheduled_callback_data_t
{ {
clock_t when; rc_clock_t when;
unsigned related_id; unsigned related_id;
rc_client_scheduled_callback_t callback; rc_client_scheduled_callback_t callback;
void* data; void* data;
@ -211,6 +221,13 @@ enum {
RC_CLIENT_SPECTATOR_MODE_LOCKED RC_CLIENT_SPECTATOR_MODE_LOCKED
}; };
enum {
RC_CLIENT_DISCONNECT_HIDDEN = 0,
RC_CLIENT_DISCONNECT_VISIBLE = (1 << 0),
RC_CLIENT_DISCONNECT_SHOW_PENDING = (1 << 1),
RC_CLIENT_DISCONNECT_HIDE_PENDING = (1 << 2)
};
struct rc_client_load_state_t; struct rc_client_load_state_t;
typedef struct rc_client_state_t { typedef struct rc_client_state_t {
@ -225,6 +242,7 @@ typedef struct rc_client_state_t {
uint8_t unofficial_enabled; uint8_t unofficial_enabled;
uint8_t log_level; uint8_t log_level;
uint8_t user; uint8_t user;
uint8_t disconnect;
struct rc_client_load_state_t* load; struct rc_client_load_state_t* load;
rc_memref_t* processing_memref; rc_memref_t* processing_memref;