From ca5e89c1215b29d34a189b54d862c6305177f651 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 30 Aug 2026 07:41:20 -0600 Subject: [PATCH 1/5] support for unpromoted leaderboards --- include/rc_api_editor.h | 2 + include/rc_api_runtime.h | 5 + include/rc_client.h | 8 ++ src/rapi/rc_api_editor.c | 20 ++- src/rapi/rc_api_runtime.c | 12 +- src/rc_client.c | 6 +- src/rc_client_internal.h | 2 + test/rapi/test_rc_api_editor.c | 55 ++++++-- test/rapi/test_rc_api_runtime.c | 6 +- test/test_rc_client.c | 239 ++++++++++++++++++++++---------- 10 files changed, 257 insertions(+), 98 deletions(-) diff --git a/include/rc_api_editor.h b/include/rc_api_editor.h index a0806932..2c47c92a 100644 --- a/include/rc_api_editor.h +++ b/include/rc_api_editor.h @@ -213,6 +213,8 @@ typedef struct rc_api_update_leaderboard_request_t { const char* format; /* Whether or not lower scores are better for the leaderboard */ uint32_t lower_is_better; + /* The leaderboard state (promoted, unpromoted) */ + uint32_t state; } rc_api_update_leaderboard_request_t; diff --git a/include/rc_api_runtime.h b/include/rc_api_runtime.h index 8a0de01b..fffec698 100644 --- a/include/rc_api_runtime.h +++ b/include/rc_api_runtime.h @@ -93,6 +93,8 @@ typedef struct rc_api_leaderboard_definition_t { const char* description; /* The definition of the leaderboard to be passed to rc_runtime_activate_lboard */ const char* definition; + /* The leaderboard state (promoted, unpromoted) */ + uint8_t state; /* Non-zero if lower values are better for this leaderboard */ uint8_t lower_is_better; /* Non-zero if the leaderboard should not be displayed in a list of leaderboards */ @@ -100,6 +102,9 @@ typedef struct rc_api_leaderboard_definition_t { } rc_api_leaderboard_definition_t; +#define RC_LEADERBOARD_STATE_ACTIVE 1 +#define RC_LEADERBOARD_STATE_UNPROMOTED 2 + /* An achievement definition */ typedef struct rc_api_achievement_definition_t { /* The unique identifier of the achievement */ diff --git a/include/rc_client.h b/include/rc_client.h index dd1538c0..cdaf489b 100644 --- a/include/rc_client.h +++ b/include/rc_client.h @@ -645,6 +645,13 @@ enum { NUM_RC_CLIENT_LEADERBOARD_FORMATS = 3 }; +enum { + RC_CLIENT_LEADERBOARD_CATEGORY_NONE = 0, + RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED = (1 << 0), + RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED = (1 << 1), + RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED_AND_UNPROMOTED = RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED | RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED +}; + #define RC_CLIENT_LEADERBOARD_DISPLAY_SIZE 24 typedef struct rc_client_leaderboard_t { @@ -655,6 +662,7 @@ typedef struct rc_client_leaderboard_t { uint8_t state; uint8_t format; uint8_t lower_is_better; + uint8_t category; } rc_client_leaderboard_t; /** diff --git a/src/rapi/rc_api_editor.c b/src/rapi/rc_api_editor.c index bb80586c..d2aab6fb 100644 --- a/src/rapi/rc_api_editor.c +++ b/src/rapi/rc_api_editor.c @@ -304,7 +304,7 @@ void rc_api_destroy_update_code_notes_response(rc_api_update_code_notes_response /* --- Update Achievement --- */ -static const char* rc_type_string(uint32_t type) { +static const char* rc_achievement_type_string(uint32_t type) { switch (type) { case RC_ACHIEVEMENT_TYPE_MISSABLE: return "missable"; case RC_ACHIEVEMENT_TYPE_PROGRESSION: return "progression"; @@ -350,7 +350,7 @@ int rc_api_init_update_achievement_request_hosted(rc_api_request_t* request, rc_url_builder_append_unum_param(&builder, "f", api_params->category); if (api_params->badge) rc_url_builder_append_str_param(&builder, "b", api_params->badge); - rc_url_builder_append_str_param(&builder, "x", rc_type_string(api_params->type)); + rc_url_builder_append_str_param(&builder, "x", rc_achievement_type_string(api_params->type)); /* Evaluate the signature. */ md5_init(&md5); @@ -391,6 +391,7 @@ int rc_api_process_update_achievement_server_response(rc_api_update_achievement_ rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), RC_JSON_NEW_FIELD("AchievementID") }; @@ -401,7 +402,7 @@ int rc_api_process_update_achievement_server_response(rc_api_update_achievement_ if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_unum(&response->achievement_id, &response->response, &fields[2], "AchievementID")) + if (!rc_json_get_required_unum(&response->achievement_id, &response->response, &fields[3], "AchievementID")) return RC_MISSING_VALUE; return RC_OK; @@ -413,6 +414,14 @@ void rc_api_destroy_update_achievement_response(rc_api_update_achievement_respon /* --- Update Leaderboard --- */ +static const char* rc_leaderboard_state_string(uint32_t type) { + switch (type) { + case RC_LEADERBOARD_STATE_ACTIVE: return "active"; + case RC_LEADERBOARD_STATE_UNPROMOTED: return "unpromoted"; + default: return ""; + } +} + int rc_api_init_update_leaderboard_request(rc_api_request_t* request, const rc_api_update_leaderboard_request_t* api_params) { return rc_api_init_update_leaderboard_request_hosted(request, api_params, &g_host); } @@ -459,6 +468,8 @@ int rc_api_init_update_leaderboard_request_hosted(rc_api_request_t* request, rc_url_builder_append_str_param(&builder, "l", api_params->value_definition); rc_url_builder_append_num_param(&builder, "w", api_params->lower_is_better); rc_url_builder_append_str_param(&builder, "f", api_params->format); + rc_url_builder_append_str_param(&builder, "m", + rc_leaderboard_state_string(api_params->state ? api_params->state : RC_LEADERBOARD_STATE_ACTIVE)); /* Evaluate the signature. */ md5_init(&md5); @@ -499,6 +510,7 @@ int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_ rc_json_field_t fields[] = { RC_JSON_NEW_FIELD("Success"), RC_JSON_NEW_FIELD("Error"), + RC_JSON_NEW_FIELD("Code"), RC_JSON_NEW_FIELD("LeaderboardID") }; @@ -509,7 +521,7 @@ int rc_api_process_update_leaderboard_server_response(rc_api_update_leaderboard_ if (result != RC_OK || !response->response.succeeded) return result; - if (!rc_json_get_required_unum(&response->leaderboard_id, &response->response, &fields[2], "LeaderboardID")) + if (!rc_json_get_required_unum(&response->leaderboard_id, &response->response, &fields[3], "LeaderboardID")) return RC_MISSING_VALUE; return RC_OK; diff --git a/src/rapi/rc_api_runtime.c b/src/rapi/rc_api_runtime.c index afecabba..2a54a8d8 100644 --- a/src/rapi/rc_api_runtime.c +++ b/src/rapi/rc_api_runtime.c @@ -234,7 +234,8 @@ static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* respon RC_JSON_NEW_FIELD("Mem"), RC_JSON_NEW_FIELD("Format"), RC_JSON_NEW_FIELD("LowerIsBetter"), - RC_JSON_NEW_FIELD("Hidden") + RC_JSON_NEW_FIELD("Hidden"), + RC_JSON_NEW_FIELD("State") }; memset(&iterator, 0, sizeof(iterator)); @@ -267,6 +268,13 @@ static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* respon leaderboard->format = RC_FORMAT_VALUE; } + leaderboard->state = RC_LEADERBOARD_STATE_ACTIVE; + if (leaderboard_fields[7].value_start && + leaderboard_fields[7].value_end - leaderboard_fields[7].value_start == 12 && + memcmp(leaderboard_fields[7].value_start + 1, "unpromoted", 10) == 0) { + leaderboard->state = RC_LEADERBOARD_STATE_UNPROMOTED; + } + ++leaderboard; } @@ -394,6 +402,8 @@ int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request, else rc_url_builder_append_str_param(&builder, "m", api_params->game_hash); + rc_url_builder_append_unum_param(&builder, "v", 2); /* indicate we want unpromoted leaderboards */ + request->post_data = rc_url_builder_finalize(&builder); request->content_type = RC_CONTENT_TYPE_URLENCODED; } diff --git a/src/rc_client.c b/src/rc_client.c index 7a739958..d3e7723b 100644 --- a/src/rc_client.c +++ b/src/rc_client.c @@ -583,7 +583,7 @@ static void rc_client_raise_disconnect_events(rc_client_t* client) client->callbacks.event_handler(&client_event, client); } -static int rc_client_should_retry(const rc_api_server_response_t* server_response) +int rc_client_should_retry(const rc_api_server_response_t* server_response) { switch (server_response->http_status_code) { case 502: /* 502 Bad Gateway */ @@ -2234,6 +2234,8 @@ static void rc_client_copy_leaderboards(rc_client_load_state_t* load_state, 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->public_.category = (read->state == RC_LEADERBOARD_STATE_ACTIVE) + ? RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED : RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED; leaderboard->format = (uint8_t)read->format; leaderboard->hidden = (uint8_t)read->hidden; @@ -5024,7 +5026,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli leaderboard = subset->leaderboards; stop = leaderboard + subset->public_.num_leaderboards; for (; leaderboard < stop; ++leaderboard) { - if (leaderboard->hidden) + if (leaderboard->hidden || leaderboard->public_.category == RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED) continue; leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); diff --git a/src/rc_client_internal.h b/src/rc_client_internal.h index fad288d6..3b04e830 100644 --- a/src/rc_client_internal.h +++ b/src/rc_client_internal.h @@ -357,6 +357,8 @@ struct rc_client_t { rc_client_state_t state; }; +int rc_client_should_retry(const rc_api_server_response_t* server_response); + /*****************************************************************************\ | Helpers | \*****************************************************************************/ diff --git a/test/rapi/test_rc_api_editor.c b/test/rapi/test_rc_api_editor.c index 8ff481df..bff8d67e 100644 --- a/test/rapi/test_rc_api_editor.c +++ b/test/rapi/test_rc_api_editor.c @@ -564,12 +564,12 @@ static void test_init_update_achievement_response() static void test_init_update_achievement_response_invalid_credentials() { rc_api_update_achievement_response_t update_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + const char* server_response = "{\"Success\":false,\"Error\":\"Invalid user/token combination\",\"Code\":\"invalid_credentials\",\"Status\":401}"; memset(&update_achievement_response, 0, sizeof(update_achievement_response)); - ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_INVALID_CREDENTIALS); ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "Credentials invalid (0)"); + ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "Invalid user/token combination"); ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); rc_api_destroy_update_achievement_response(&update_achievement_response); @@ -578,10 +578,10 @@ static void test_init_update_achievement_response_invalid_credentials() static void test_init_update_achievement_response_invalid_perms() { rc_api_update_achievement_response_t update_achievement_response; - const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; + const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\",\"Code\":\"access_denied\",\"Status\":403}"; memset(&update_achievement_response, 0, sizeof(update_achievement_response)); - ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(rc_api_process_update_achievement_response(&update_achievement_response, server_response), RC_ACCESS_DENIED); ASSERT_NUM_EQUALS(update_achievement_response.response.succeeded, 0); ASSERT_STR_EQUALS(update_achievement_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); ASSERT_UNUM_EQUALS(update_achievement_response.achievement_id, 0); @@ -610,7 +610,7 @@ static void test_init_update_leaderboard_request() ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&m=active&h=bbdb85cb1eb82773d5740c2d5d515ec0"); ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); rc_api_destroy_request(&request); @@ -633,10 +633,11 @@ static void test_init_update_leaderboard_request_new() update_leaderboard_request.value_definition = "0xH2345"; update_leaderboard_request.lower_is_better = 1; update_leaderboard_request.format = "SCORE"; + update_leaderboard_request.state = RC_LEADERBOARD_STATE_ACTIVE; ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=739e28608a9e93d7351103d2f43fc6dc"); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&m=active&h=739e28608a9e93d7351103d2f43fc6dc"); ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); rc_api_destroy_request(&request); @@ -685,7 +686,34 @@ static void test_init_update_leaderboard_request_no_description() ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&i=5555&g=1234&n=Title&d=&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&m=active&h=bbdb85cb1eb82773d5740c2d5d515ec0"); + ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); + + rc_api_destroy_request(&request); +} + +static void test_init_update_leaderboard_request_unpromoted() +{ + rc_api_update_leaderboard_request_t update_leaderboard_request; + rc_api_request_t request; + + memset(&update_leaderboard_request, 0, sizeof(update_leaderboard_request)); + update_leaderboard_request.username = "Dev"; + update_leaderboard_request.api_token = "API_TOKEN"; + update_leaderboard_request.game_id = 1234; + update_leaderboard_request.title = "Title"; + update_leaderboard_request.description = "Description"; + update_leaderboard_request.start_trigger = "0xH1234=1"; + update_leaderboard_request.submit_trigger = "0xH1234=2"; + update_leaderboard_request.cancel_trigger = "0xH1234=3"; + update_leaderboard_request.value_definition = "0xH2345"; + update_leaderboard_request.lower_is_better = 1; + update_leaderboard_request.format = "SCORE"; + update_leaderboard_request.state = RC_LEADERBOARD_STATE_UNPROMOTED; + + ASSERT_NUM_EQUALS(rc_api_init_update_leaderboard_request(&request, &update_leaderboard_request), RC_OK); + ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); + ASSERT_STR_EQUALS(request.post_data, "r=uploadleaderboard&u=Dev&t=API_TOKEN&g=1234&n=Title&d=Description&s=0xH1234%3d1&b=0xH1234%3d2&c=0xH1234%3d3&l=0xH2345&w=1&f=SCORE&m=unpromoted&h=739e28608a9e93d7351103d2f43fc6dc"); ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); rc_api_destroy_request(&request); @@ -708,12 +736,12 @@ static void test_init_update_leaderboard_response() static void test_init_update_leaderboard_response_invalid_credentials() { rc_api_update_leaderboard_response_t update_leaderboard_response; - const char* server_response = "{\"Success\":false,\"Error\":\"Credentials invalid (0)\"}"; + const char* server_response = "{\"Success\":false,\"Error\":\"Invalid user/token combination\",\"Code\":\"invalid_credentials\",\"Status\":401}"; memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); - ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_INVALID_CREDENTIALS); ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); - ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "Credentials invalid (0)"); + ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "Invalid user/token combination"); ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); rc_api_destroy_update_leaderboard_response(&update_leaderboard_response); @@ -722,10 +750,10 @@ static void test_init_update_leaderboard_response_invalid_credentials() static void test_init_update_leaderboard_response_invalid_perms() { rc_api_update_leaderboard_response_t update_leaderboard_response; - const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\"}"; + const char* server_response = "{\"Success\":false,\"Error\":\"You must be a developer to perform this action! Please drop a message in the forums to apply.\",\"Code\":\"access_denied\",\"Status\":403}"; memset(&update_leaderboard_response, 0, sizeof(update_leaderboard_response)); - ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_OK); + ASSERT_NUM_EQUALS(rc_api_process_update_leaderboard_response(&update_leaderboard_response, server_response), RC_ACCESS_DENIED); ASSERT_NUM_EQUALS(update_leaderboard_response.response.succeeded, 0); ASSERT_STR_EQUALS(update_leaderboard_response.response.error_message, "You must be a developer to perform this action! Please drop a message in the forums to apply."); ASSERT_UNUM_EQUALS(update_leaderboard_response.leaderboard_id, 0); @@ -1055,6 +1083,7 @@ void test_rapi_editor(void) { TEST(test_init_update_leaderboard_request_new); TEST(test_init_update_leaderboard_request_no_game_id); TEST(test_init_update_leaderboard_request_no_description); + TEST(test_init_update_leaderboard_request_unpromoted); TEST(test_init_update_leaderboard_response); TEST(test_init_update_leaderboard_response_invalid_credentials); diff --git a/test/rapi/test_rc_api_runtime.c b/test/rapi/test_rc_api_runtime.c index dbe56f2a..4cf83994 100644 --- a/test/rapi/test_rc_api_runtime.c +++ b/test/rapi/test_rc_api_runtime.c @@ -693,7 +693,7 @@ static void test_init_fetch_game_sets_request() { ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&m=ABCDEF0123456789"); + ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&m=ABCDEF0123456789&v=2"); ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); rc_api_destroy_request(&request); @@ -723,7 +723,7 @@ static void test_init_fetch_game_sets_request_by_id() { ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953"); + ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953&v=2"); ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); rc_api_destroy_request(&request); @@ -741,7 +741,7 @@ static void test_init_fetch_game_sets_request_by_hash_and_id() { ASSERT_NUM_EQUALS(rc_api_init_fetch_game_sets_request(&request, &fetch_game_sets_request), RC_OK); ASSERT_STR_EQUALS(request.url, DOREQUEST_URL); - ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953"); + ASSERT_STR_EQUALS(request.post_data, "r=achievementsets&u=Username&t=API_TOKEN&g=953&v=2"); ASSERT_STR_EQUALS(request.content_type, RC_CONTENT_TYPE_URLENCODED); rc_api_destroy_request(&request); diff --git a/test/test_rc_client.c b/test/test_rc_client.c index 2975c7f6..776ec6c0 100644 --- a/test/test_rc_client.c +++ b/test/test_rc_client.c @@ -41,7 +41,13 @@ static void* g_callback_userdata = &g_client; /* dummy object to use for callbac "\"Author\":\"User1\",\"BadgeName\":\"00" id "\",\"Created\":1367266583,\"Modified\":1376929305}" #define GENERIC_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ - "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\"}" + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"State\":\"active\"}" + +#define HIDDEN_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"State\":\"active\",\"Hidden\":true}" + +#define UNPROMOTED_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ + "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"State\":\"unpromoted\"}" static const char* patchdata_empty = "{\"Success\":true," "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," @@ -247,9 +253,6 @@ static const char* patchdata_big_ids = "{\"Success\":true," "\"Leaderboards\":[]" "}]}"; -#define HIDDEN_LEADERBOARD_JSON(id, memaddr, format) "{\"ID\":" id ",\"Title\":\"Leaderboard " id "\"," \ - "\"Description\":\"Desc " id "\",\"Mem\":\"" memaddr "\",\"Format\":\"" format "\",\"Hidden\":true}" - static const char* patchdata_leaderboards_hidden = "{\"Success\":true," "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," "\"ImageIconUrl\":\"http://server/Images/112233.png\"," @@ -267,6 +270,23 @@ static const char* patchdata_leaderboards_hidden = "{\"Success\":true," "]" "}]}"; +static const char* patchdata_leaderboards_unpromoted = "{\"Success\":true," + "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"RichPresenceGameId\":1234,\"RichPresencePatch\":\"Display:\\r\\nPoints:@Number(0xH0003)\\r\\n\"," + "\"Sets\":[{" + "\"AchievementSetId\":1111,\"GameId\":1234,\"Title\":null,\"Type\":\"core\"," + "\"ImageIconUrl\":\"http://server/Images/112233.png\"," + "\"Achievements\":[]," + "\"Leaderboards\":[" + GENERIC_LEADERBOARD_JSON("44", "STA:0xH000B=1::CAN:0xH000C=1::SUB:0xH000D=1::VAL:0x 000E", "SCORE") "," + UNPROMOTED_LEADERBOARD_JSON("45", "STA:0xH000A=1::CAN:0xH000C=2::SUB:0xH000D=1::VAL:0xH000E", "SCORE") "," + GENERIC_LEADERBOARD_JSON("46", "STA:0xH000A=1::CAN:0xH000C=3::SUB:0xH000D=1::VAL:0x 000E", "VALUE") "," + GENERIC_LEADERBOARD_JSON("47", "STA:0xH000A=1::CAN:0xH000C=4::SUB:0xH000D=2::VAL:0x 000E", "SCORE") "," + UNPROMOTED_LEADERBOARD_JSON("48", "STA:0xH000A=2::CAN:0xH000C=5::SUB:0xH000D=1::VAL:0x 000E", "SCORE") + "]" + "}]}"; + static const char* patchdata_unofficial_unsupported = "{\"Success\":true," "\"GameId\":1234,\"Title\":\"Sample Game\",\"ConsoleId\":17," "\"ImageIconUrl\":\"http://server/Images/112233.png\"," @@ -797,7 +817,7 @@ static void mock_client_load_game(const char* patchdata, const char* unlocks) { reset_mock_api_handlers(); event_count = 0; - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlocks); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); @@ -810,7 +830,7 @@ static void mock_client_load_game_softcore(const char* patchdata, const char* un { reset_mock_api_handlers(); event_count = 0; - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=0&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlocks); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); @@ -1168,7 +1188,7 @@ static void test_logout(void) /* attempt to load game should fail */ reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", "{\"Success\":true,\"GameID\":1234}"); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_required, g_callback_userdata); @@ -1235,7 +1255,7 @@ static void test_logout_during_fetch_game(void) rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_no_longer_active, g_callback_userdata); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); rc_client_logout(g_client); @@ -1316,7 +1336,7 @@ static void test_get_user_game_summary_encore_mode(void) g_client = mock_client_logged_in(); rc_client_set_unofficial_enabled(g_client, 1); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_exhaustive); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_exhaustive); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, unlock_6_8h_and_9); rc_client_set_encore_mode_enabled(g_client, 1); @@ -1446,7 +1466,7 @@ static void test_get_user_game_summary_unknown_game(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_not_found); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); rc_client_get_user_game_summary(g_client, &summary); @@ -1653,7 +1673,7 @@ static void test_load_game_unknown_hash(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_not_found); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); ASSERT_NUM_EQUALS(rc_client_is_game_loaded(g_client), 0); @@ -1698,7 +1718,7 @@ static void test_load_game_unknown_hash_repeated(void) ASSERT_PTR_NOT_NULL(handle); ASSERT_PTR_NOT_NULL(g_client->state.load); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -1745,7 +1765,7 @@ static void test_load_game_unknown_hash_multiple(void) ASSERT_PTR_NOT_NULL(handle); ASSERT_PTR_NOT_NULL(g_client->state.load); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -1764,7 +1784,7 @@ static void test_load_game_unknown_hash_multiple(void) ASSERT_PTR_NOT_NULL(handle); ASSERT_PTR_NOT_NULL(g_client->state.load); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=FEDCBA9876543210", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=FEDCBA9876543210&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -1785,7 +1805,7 @@ static void test_load_game_not_logged_in(void) g_client = mock_client_not_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", "{\"Success\":true,\"GameID\":1234}"); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", "{\"Success\":true,\"GameID\":1234}"); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); @@ -1805,7 +1825,7 @@ static void test_load_game(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); @@ -1892,16 +1912,16 @@ static void test_load_game_async_load_different_game(void) /* start loading first game */ ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_NONE); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_no_longer_active, g_callback_userdata); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); /* receive data for first game, start session for first game */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); /* start loading second game*/ rc_client_begin_load_game(g_client, "ABCDEF0123456789", rc_client_callback_expect_success, g_callback_userdata); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789&v=2"); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); /* session started for first game, should abort */ @@ -1909,7 +1929,7 @@ static void test_load_game_async_load_different_game(void) ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); /* receive data for second game, start session for second game */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789", patchdata_alternate); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=ABCDEF0123456789&v=2", patchdata_alternate); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); /* session started for second game, should succeed */ @@ -1946,15 +1966,15 @@ static void test_load_game_async_login(void) ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN); /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); /* login completion will trigger process to continue */ async_api_response("r=login2&u=Username&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_IDENTIFYING_GAME); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_STARTING_SESSION); async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); @@ -1989,13 +2009,13 @@ static void test_load_game_async_login_with_incorrect_password(void) ASSERT_NUM_EQUALS(rc_client_get_load_game_state(g_client), RC_CLIENT_LOAD_GAME_STATE_AWAIT_LOGIN); /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); /* login failure will trigger process to continue */ async_api_error("r=login2&u=Username&p=Pa%24%24word", "{\"Success\":false,\"Error\":\"Invalid User/Password combination. Please try again\"," "\"Status\":401,\"Code\":\"invalid_credentials\"}", 401); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); ASSERT_PTR_NULL(g_client->user.username); @@ -2015,13 +2035,13 @@ static void test_load_game_async_login_logout(void) rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_aborted, g_callback_userdata); /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); /* logout will cancel login and allow game load to proceed with failure */ rc_client_logout(g_client); async_api_response("r=login2&u=Username&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); ASSERT_PTR_NULL(g_client->user.username); @@ -2043,13 +2063,13 @@ static void test_load_game_async_login_aborted(void) rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_login_aborted, g_callback_userdata); /* game load process will stop here waiting for the login to complete */ - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); /* login abort will trigger game load process to continue */ rc_client_abort_async(g_client, handle); async_api_response("r=login2&u=Username&p=Pa%24%24word", "{\"Success\":true,\"User\":\"Username\",\"Token\":\"ApiToken\",\"Score\":12345,\"SoftcoreScore\":123,\"Messages\":2,\"Permissions\":1,\"AccountType\":\"Registered\"}"); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2"); ASSERT_PTR_NULL(g_client->user.username); @@ -2073,7 +2093,7 @@ static void test_load_game_patch_failure(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_error("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", response_429, 429); + mock_api_error("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", response_429, 429); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); @@ -2090,7 +2110,7 @@ static void test_load_game_startsession_failure(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, response_429, 429); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_too_many_requests, g_callback_userdata); @@ -2107,7 +2127,7 @@ static void test_load_game_startsession_timeout(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "", 504); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_timeout, g_callback_userdata); @@ -2124,7 +2144,7 @@ static void test_load_game_startsession_custom_timeout(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); mock_api_error("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "Request has timed out.", RC_API_SERVER_RESPONSE_RETRYABLE_CLIENT_ERROR); @@ -2151,7 +2171,7 @@ static void test_load_game_patch_aborted(void) rc_client_abort_async(g_client, handle); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); assert_api_not_called("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING); ASSERT_PTR_NULL(g_client->state.load); @@ -2173,7 +2193,7 @@ static void test_load_game_startsession_aborted(void) handle = rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); rc_client_abort_async(g_client, handle); @@ -2194,7 +2214,7 @@ static void test_load_game_while_spectating(void) rc_client_set_spectator_mode_enabled(g_client, 1); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); /* spectator mode should not start a session or fetch unlocks */ rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); @@ -2280,7 +2300,7 @@ static void test_load_game_process_game_sets(void) rc_client_callback_process_game_sets_called = 0; reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); @@ -2344,7 +2364,7 @@ static void test_load_game_destroy_while_fetching_game_data(void) rc_client_destroy(g_client); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); } static void test_load_unknown_game(void) @@ -2407,7 +2427,7 @@ static void test_load_game_dispatched_read_memory(void) rc_client_set_allow_background_memory_reads(g_client, 0); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); @@ -2509,7 +2529,7 @@ static void test_unload_game_while_fetching_game_data(void) rc_client_unload_game(g_client); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NULL(g_client->game); @@ -2525,7 +2545,7 @@ static void test_unload_game_while_starting_session(void) rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_uncalled, g_callback_userdata); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_2ach_1lbd); rc_client_unload_game(g_client); @@ -2570,7 +2590,7 @@ static void test_identify_and_load_game_console_specified(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", @@ -2602,7 +2622,7 @@ static void test_identify_and_load_game_console_not_specified(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", @@ -2644,17 +2664,17 @@ static void test_identify_and_load_game_multiconsole_first(void) image, image_size, rc_client_callback_expect_success, g_callback_userdata); /* first hash lookup should be pending. inject a secondary console into the iterator */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2"); iterator = rc_client_get_load_state_hash_iterator(g_client); ASSERT_NUM_EQUALS(iterator->index, 1); ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ iterator->consoles[iterator->index + 1] = 0; - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_2ach_1lbd); async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); - assert_api_not_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); + assert_api_not_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e&v=2"); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -2689,17 +2709,17 @@ static void test_identify_and_load_game_multiconsole_second(void) image, image_size, rc_client_callback_expect_success, g_callback_userdata); /* first hash lookup should be pending. inject a secondary console into the iterator */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2"); iterator = rc_client_get_load_state_hash_iterator(g_client); ASSERT_NUM_EQUALS(iterator->index, 1); ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ iterator->consoles[iterator->index + 1] = 0; - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e" ,patchdata_2ach_1lbd); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e&v=2"); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e&v=2" ,patchdata_2ach_1lbd); async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=64b131c5c7fec32985d9c99700babb7e&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); ASSERT_PTR_NULL(g_client->state.load); @@ -2745,7 +2765,7 @@ static void test_identify_and_load_game_from_disc(void) mock_file(1, "game.cue", (uint8_t*)cue_single_track, sizeof(cue_single_track)); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=db433fb038cde4fb15c144e8c7dea6e3", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=db433fb038cde4fb15c144e8c7dea6e3&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=db433fb038cde4fb15c144e8c7dea6e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_PLAYSTATION, "game.cue", @@ -2780,7 +2800,7 @@ static void test_identify_and_load_game_unknown_hash(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "foo.zip#foo.gb", image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); @@ -2819,7 +2839,7 @@ static void test_identify_and_load_game_unknown_hash_repeated(void) ASSERT_PTR_NOT_NULL(handle); ASSERT_PTR_NOT_NULL(g_client->state.load); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -2869,7 +2889,7 @@ static void test_identify_and_load_game_unknown_hash_multiple(void) ASSERT_PTR_NOT_NULL(handle); ASSERT_PTR_NOT_NULL(g_client->state.load); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=88be638f4d78b4072109e55f13e8a0ac", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=88be638f4d78b4072109e55f13e8a0ac&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -2889,7 +2909,7 @@ static void test_identify_and_load_game_unknown_hash_multiple(void) ASSERT_PTR_NOT_NULL(handle); ASSERT_PTR_NOT_NULL(g_client->state.load); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=8e39c6077108cafd6193d1c649b5d695", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=8e39c6077108cafd6193d1c649b5d695&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -2923,17 +2943,17 @@ static void test_identify_and_load_game_unknown_hash_multiconsole(void) image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); /* first hash lookup should be pending. inject a secondary console into the iterator */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2"); iterator = rc_client_get_load_state_hash_iterator(g_client); ASSERT_NUM_EQUALS(iterator->index, 1); ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); iterator->consoles[iterator->index] = RC_CONSOLE_MEGA_DRIVE; /* full buffer hash */ iterator->consoles[iterator->index + 1] = 0; - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e", patchdata_not_found); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e&v=2"); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=64b131c5c7fec32985d9c99700babb7e&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -2970,12 +2990,12 @@ static void test_identify_and_load_game_unknown_hash_console_specified(void) image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); /* first hash lookup should be pending. iterator should not have been initialized */ - assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + assert_api_pending("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2"); iterator = rc_client_get_load_state_hash_iterator(g_client); ASSERT_NUM_EQUALS(iterator->index, 0); ASSERT_NUM_EQUALS(iterator->consoles[iterator->index], 0); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); ASSERT_PTR_NULL(g_client->state.load); ASSERT_PTR_NOT_NULL(g_client->game); @@ -3016,8 +3036,8 @@ static void test_identify_and_load_game_unknown_hash_client_provided(void) g_client->callbacks.identify_unknown_hash = rc_client_identify_unknown_hash; reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&g=1234", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&g=1234&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_GAMEBOY, "foo.zip#foo.gb", @@ -3050,7 +3070,7 @@ static void test_identify_and_load_game_multihash(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_2ach_1lbd); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_2ach_1lbd); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=6a2305a2b6675a97ff792709be1ca857&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", @@ -3082,7 +3102,7 @@ static void test_identify_and_load_game_multihash_unknown_game(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); rc_client_begin_identify_and_load_game(g_client, RC_CONSOLE_UNKNOWN, "abc.dsk", image, image_size, rc_client_callback_expect_unknown_game, g_callback_userdata); @@ -3100,7 +3120,7 @@ static void test_identify_and_load_game_multihash_unknown_game(void) } /* same hash generated for all dsk consoles - only one server call should be made */ - assert_api_call_count("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", 1); + assert_api_call_count("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", 1); rc_client_destroy(g_client); free(image); @@ -3123,11 +3143,11 @@ static void test_identify_and_load_game_multihash_differ(void) memset(&image[256], 0, 32); /* first lookup fails */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857", patchdata_not_found); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2", patchdata_not_found); ASSERT_PTR_NOT_NULL(g_client->state.load); /* second lookup should succeed */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3&v=2", patchdata_2ach_1lbd); async_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=4989b063a40dcfa28291ff8d675050e3&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); ASSERT_PTR_NULL(g_client->state.load); @@ -3483,7 +3503,7 @@ static void test_change_media_while_loading(void) /* media request won't occur until patch data is received */ assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3&v=2", patchdata_2ach_1lbd); assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); /* finish loading game */ @@ -3525,7 +3545,7 @@ static void test_change_media_while_loading_later(void) rc_client_callback_expect_success, g_callback_userdata); /* get past fetching the patch data so there's a valid console for the change media call */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3&v=2", patchdata_2ach_1lbd); /* change_media should immediately attempt to resolve the new hash */ rc_client_begin_identify_and_change_media(g_client, "foo.zip#foo.gb", image, image_size, @@ -3594,7 +3614,7 @@ static void test_change_media_async_aborted(void) rc_client_callback_expect_success, g_callback_userdata); ASSERT_STR_EQUALS(g_client->game->public_.hash, "6a2305a2b6675a97ff792709be1ca857"); - assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857"); + assert_api_not_called("r=achievementsets&u=Username&t=ApiToken&m=6a2305a2b6675a97ff792709be1ca857&v=2"); rc_client_destroy(g_client); free(image); @@ -3791,7 +3811,7 @@ static void test_change_media_from_hash_while_loading(void) /* media request won't occur until patch data is received */ assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3&v=2", patchdata_2ach_1lbd); assert_api_not_called("r=gameid&m=6a2305a2b6675a97ff792709be1ca857"); /* finish loading game */ @@ -3822,7 +3842,7 @@ static void test_change_media_from_hash_while_loading_later(void) rc_client_callback_expect_success, g_callback_userdata); /* get past fetching the patch data so there's a valid console for the change media call */ - async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3", patchdata_2ach_1lbd); + async_api_response("r=achievementsets&u=Username&t=ApiToken&m=4989b063a40dcfa28291ff8d675050e3&v=2", patchdata_2ach_1lbd); /* change_media should immediately attempt to resolve the new hash */ rc_client_begin_change_media(g_client, "6a2305a2b6675a97ff792709be1ca857", @@ -4107,7 +4127,7 @@ static void test_load_subset(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_subset); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_subset); mock_api_response("r=startsession&u=Username&t=ApiToken&g=1234&h=1&m=0123456789ABCDEF&l=" RCHEEVOS_VERSION_STRING, "{\"Success\":true}"); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_success, g_callback_userdata); @@ -5920,6 +5940,74 @@ static void test_leaderboard_list_hidden(void) rc_client_destroy(g_client); } +static void test_leaderboard_list_unpromoted(void) +{ + rc_client_leaderboard_list_t* list; + const rc_client_leaderboard_t** iter; + const rc_client_leaderboard_t* leaderboard; + uint8_t memory[16] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0 }; + + g_client = mock_client_logged_in(); + mock_memory(memory, sizeof(memory)); + mock_client_load_game(patchdata_leaderboards_unpromoted, no_unlocks); + + rc_client_do_frame(g_client); + + /* unpromoted leaderboards (45+48) should not appear in list */ + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 3); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + rc_client_destroy_leaderboard_list(list); + } + + memory[0x0A] = 1; /* start 45,46,47 */ + rc_client_do_frame(g_client); + + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active"); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + + iter = list->buckets[0].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 46); + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 47); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[1].num_leaderboards, 1); + + iter = list->buckets[1].leaderboards; + leaderboard = *iter++; + ASSERT_NUM_EQUALS(leaderboard->id, 44); + + rc_client_destroy_leaderboard_list(list); + } + + rc_client_destroy(g_client); +} + static const char* lbinfo_4401_top_10 = "{\"Success\":true,\"LeaderboardData\":{\"LBID\":4401,\"GameID\":1234," "\"LowerIsBetter\":1,\"LBTitle\":\"Leaderboard1\",\"LBDesc\":\"Desc1\",\"LBFormat\":\"SCORE\"," "\"LBMem\":\"STA:0xH000C=1::CAN:0xH000D=1::SUB:0xH000D=2::VAL:0x 000E\",\"LBAuthor\":null," @@ -10010,7 +10098,7 @@ static void test_deserialize_progress_unknown_game(void) g_client = mock_client_logged_in(); reset_mock_api_handlers(); - mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF", patchdata_not_found); + mock_api_response("r=achievementsets&u=Username&t=ApiToken&m=0123456789ABCDEF&v=2", patchdata_not_found); rc_client_begin_load_game(g_client, "0123456789ABCDEF", rc_client_callback_expect_unknown_game, g_callback_userdata); ASSERT_NUM_EQUALS(rc_client_progress_size(g_client), 0); @@ -10624,6 +10712,7 @@ void test_client(void) { TEST(test_leaderboard_list_buckets_with_unsupported); TEST(test_leaderboard_list_subset); TEST(test_leaderboard_list_hidden); + TEST(test_leaderboard_list_unpromoted); TEST(test_fetch_leaderboard_entries); TEST(test_fetch_leaderboard_entries_no_user); From bc6e9d3879e78c868be837e3dc243cf5f2ad8e9a Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 3 Sep 2026 08:47:37 -0600 Subject: [PATCH 2/5] rc_client_external mapping support --- include/rc_client.h | 15 +- src/rc_client.c | 74 +++++++-- src/rc_client_external.c | 122 ++++++++++++++ src/rc_client_external.h | 12 +- src/rc_client_external_versions.h | 63 +++++++ src/rc_client_internal.h | 2 + test/test_rc_client_external.c | 268 +++++++++++++++++++++++------- 7 files changed, 473 insertions(+), 83 deletions(-) diff --git a/include/rc_client.h b/include/rc_client.h index cdaf489b..ab9cd949 100644 --- a/include/rc_client.h +++ b/include/rc_client.h @@ -695,7 +695,8 @@ enum { RC_CLIENT_LEADERBOARD_BUCKET_ACTIVE = 2, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED = 3, RC_CLIENT_LEADERBOARD_BUCKET_ALL = 4, - NUM_RC_CLIENT_LEADERBOARD_BUCKETS = 5 + RC_CLIENT_LEADERBOARD_BUCKET_UNPROMOTED = 5, + NUM_RC_CLIENT_LEADERBOARD_BUCKETS = 6 }; enum { @@ -709,6 +710,18 @@ enum { */ RC_EXPORT rc_client_leaderboard_list_t* RC_CCONV rc_client_create_leaderboard_list(rc_client_t* client, int grouping); +typedef struct rc_client_leaderboard_list_params_t { + uint32_t subset_id; + uint32_t grouping; + uint32_t category; +} rc_client_leaderboard_list_params_t; + +/** + * Creates a list of leaderboards matching the specified parameters. + * Returns an allocated list that must be free'd by calling rc_client_destroy_leaderboard_list. + */ +RC_EXPORT rc_client_leaderboard_list_t* RC_CCONV rc_client_create_subset_leaderboard_list(rc_client_t* client, const rc_client_leaderboard_list_params_t* params); + /** * Destroys a list allocated by rc_client_create_leaderboard_list. */ diff --git a/src/rc_client.c b/src/rc_client.c index d3e7723b..4273c156 100644 --- a/src/rc_client.c +++ b/src/rc_client.c @@ -3617,6 +3617,8 @@ rc_client_subset_list_t* rc_client_create_subset_list(rc_client_t* client) } list = (rc_client_subset_list_info_t*)malloc(list_size + num_subsets * sizeof(rc_client_subset_t*)); + if (!list) + return NULL; list->public_.subsets = subset_ptr = (const rc_client_subset_t**)((uint8_t*)list + list_size); subset = client->game->subsets; @@ -4907,8 +4909,13 @@ const rc_client_leaderboard_t* rc_client_get_leaderboard_info(const rc_client_t* 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); + if (client->state.external_client) { + if (client->state.external_client->get_leaderboard_info_v8) + return client->state.external_client->get_leaderboard_info_v8(id); + + if (client->state.external_client->get_leaderboard_info) + return rc_client_external_convert_v1_leaderboard(client, client->state.external_client->get_leaderboard_info(id)); + } #endif if (!client->game) @@ -4971,12 +4978,42 @@ static uint8_t rc_client_get_leaderboard_bucket(const rc_client_leaderboard_info 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; + if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_NONE) + return RC_CLIENT_LEADERBOARD_BUCKET_ALL; + + if (leaderboard->public_.category == RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED) + return RC_CLIENT_LEADERBOARD_BUCKET_UNPROMOTED; + + return RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; } } +static int rc_client_is_subset_visible_in_leaderboard_list(const rc_client_subset_info_t* subset, const rc_client_leaderboard_list_params_t* params) +{ + if (!subset->active) + return 0; + + return (params->subset_id == 0 || params->subset_id == subset->public_.id); +} + +static int rc_client_is_leaderboard_visible_in_list(const rc_client_leaderboard_info_t* leaderboard, const rc_client_leaderboard_list_params_t* params) +{ + if (leaderboard->hidden) + return 0; + + return (leaderboard->public_.category & params->category) != 0; +} + rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* client, int grouping) +{ + rc_client_leaderboard_list_params_t params; + params.subset_id = 0; + params.category = RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED; + params.grouping = grouping; + return rc_client_create_subset_leaderboard_list(client, ¶ms); +} + +rc_client_leaderboard_list_t* rc_client_create_subset_leaderboard_list(rc_client_t* client, const rc_client_leaderboard_list_params_t* params) { rc_client_leaderboard_info_t* leaderboard; rc_client_leaderboard_info_t* stop; @@ -4999,6 +5036,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli const uint8_t subset_bucket_order[] = { RC_CLIENT_LEADERBOARD_BUCKET_ALL, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE, + RC_CLIENT_LEADERBOARD_BUCKET_UNPROMOTED, RC_CLIENT_LEADERBOARD_BUCKET_UNSUPPORTED }; @@ -5006,8 +5044,14 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli return (rc_client_leaderboard_list_t*)calloc(1, list_size); #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); + if (client->state.external_client) { + if (client->state.external_client->create_subset_leaderboard_list) + return (rc_client_leaderboard_list_t*)client->state.external_client->create_subset_leaderboard_list(params); + + if (client->state.external_client->create_leaderboard_list) + return rc_client_external_convert_v1_leaderboard_list(client, + (rc_client_leaderboard_list_t*)client->state.external_client->create_leaderboard_list(params->grouping)); + } #endif if (!client->game) @@ -5019,17 +5063,17 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli subset = client->game->subsets; for (; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_leaderboard_list(subset, params)) continue; num_subsets++; leaderboard = subset->leaderboards; stop = leaderboard + subset->public_.num_leaderboards; for (; leaderboard < stop; ++leaderboard) { - if (leaderboard->hidden || leaderboard->public_.category == RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED) + if (!rc_client_is_leaderboard_visible_in_list(leaderboard, params)) continue; - leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, grouping); + leaderboard->bucket = rc_client_get_leaderboard_bucket(leaderboard, params->grouping); bucket_counts[leaderboard->bucket]++; } } @@ -5058,7 +5102,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli subset = client->game->subsets; for (; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_leaderboard_list(subset, params)) continue; leaderboard = subset->leaderboards; @@ -5079,7 +5123,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli list->public_.buckets = bucket_ptr = (rc_client_leaderboard_bucket_t*)((uint8_t*)list + list_size); leaderboard_ptr = (const rc_client_leaderboard_t**)((uint8_t*)bucket_ptr + buckets_size); - if (grouping == RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING) { + if (params->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]) @@ -5087,13 +5131,13 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli bucket_leaderboards = leaderboard_ptr; for (subset = client->game->subsets; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_leaderboard_list(subset, params)) continue; leaderboard = subset->leaderboards; stop = leaderboard + subset->public_.num_leaderboards; for (; leaderboard < stop; ++leaderboard) { - if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + if (leaderboard->bucket == bucket_type && rc_client_is_leaderboard_visible_in_list(leaderboard, params)) *leaderboard_ptr++ = &leaderboard->public_; } } @@ -5110,7 +5154,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli } for (subset = client->game->subsets; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_leaderboard_list(subset, params)) continue; for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { @@ -5123,7 +5167,7 @@ rc_client_leaderboard_list_t* rc_client_create_leaderboard_list(rc_client_t* cli leaderboard = subset->leaderboards; stop = leaderboard + subset->public_.num_leaderboards; for (; leaderboard < stop; ++leaderboard) { - if (leaderboard->bucket == bucket_type && !leaderboard->hidden) + if (leaderboard->bucket == bucket_type && rc_client_is_leaderboard_visible_in_list(leaderboard, params)) *leaderboard_ptr++ = &leaderboard->public_; } diff --git a/src/rc_client_external.c b/src/rc_client_external.c index 88abcc9f..49539533 100644 --- a/src/rc_client_external.c +++ b/src/rc_client_external.c @@ -19,6 +19,7 @@ typedef struct rc_client_external_conversions_t { rc_client_game_t game; rc_client_subset_t subsets[4]; rc_client_achievement_t achievements[16]; + rc_client_leaderboard_t leaderboards[16]; char user_avatar_url[RC_CLIENT_USER_IMAGE_URL_BUFFER_SIZE]; char game_badge_url[RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; char subset_badge_url[4][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; @@ -26,6 +27,7 @@ typedef struct rc_client_external_conversions_t { char achievement_badge_locked_url[16][RC_CLIENT_IMAGE_URL_BUFFER_SIZE]; uint32_t next_subset_index; uint32_t next_achievement_index; + uint32_t next_leaderboard_index; } rc_client_external_conversions_t; static const char* rc_client_external_build_avatar_url(char buffer[], size_t buffer_size, uint32_t image_type, const char* image_name) @@ -279,4 +281,124 @@ rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(con return (rc_client_achievement_list_t*)new_list; } +const rc_client_leaderboard_t* rc_client_external_convert_v1_leaderboard(const rc_client_t* client, const rc_client_leaderboard_t* v1_leaderboard) +{ + rc_client_leaderboard_t* converted = NULL; + char* badge_url = NULL; + char* badge_locked_url = NULL; + const uint32_t num_leaderboards = sizeof(client->state.external_client_conversions->leaderboards) / sizeof(client->state.external_client_conversions->leaderboards[0]); + uint32_t index; + + if (!v1_leaderboard) + return NULL; + + rc_client_external_conversions_init(client); + + for (index = 0; index < num_leaderboards; ++index) { + if (client->state.external_client_conversions->leaderboards[index].id == v1_leaderboard->id) { + converted = &client->state.external_client_conversions->leaderboards[index]; + break; + } + } + + if (!converted) { + index = client->state.external_client_conversions->next_leaderboard_index; + converted = &client->state.external_client_conversions->leaderboards[index]; + client->state.external_client_conversions->next_leaderboard_index = (index + 1) % num_leaderboards; + } + + memcpy(converted, v1_leaderboard, sizeof(v1_rc_client_leaderboard_t)); + RC_CONVERSION_FILL(converted, rc_client_leaderboard_t, v1_rc_client_leaderboard_t); + + converted->category = RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED; + + return converted; +} + +typedef struct rc_client_leaderboard_list_wrapper_t { + rc_client_leaderboard_list_info_t info; + rc_client_leaderboard_list_t* source_list; + rc_client_leaderboard_t* leaderboards; + rc_client_leaderboard_t** leaderboards_pointers; +} rc_client_leaderboard_list_wrapper_t; + +static void rc_client_destroy_leaderboard_list_wrapper(rc_client_leaderboard_list_info_t* info) +{ + rc_client_leaderboard_list_wrapper_t* wrapper = (rc_client_leaderboard_list_wrapper_t*)info; + + if (wrapper->leaderboards) + free(wrapper->leaderboards); + if (wrapper->leaderboards_pointers) + free(wrapper->leaderboards_pointers); + if (wrapper->info.public_.buckets) + free((void*)wrapper->info.public_.buckets); + + rc_client_destroy_leaderboard_list(wrapper->source_list); + + free(wrapper); +} + +rc_client_leaderboard_list_t* rc_client_external_convert_v1_leaderboard_list(const rc_client_t* client, rc_client_leaderboard_list_t* v1_leaderboard_list) +{ + rc_client_leaderboard_list_wrapper_t* new_list; + (void)client; + + if (!v1_leaderboard_list) + return NULL; + + new_list = (rc_client_leaderboard_list_wrapper_t*)calloc(1, sizeof(*new_list)); + if (!new_list) + return NULL; + + new_list->source_list = v1_leaderboard_list; + new_list->info.destroy_func = rc_client_destroy_leaderboard_list_wrapper; + + if (v1_leaderboard_list->num_buckets) { + const v1_rc_client_leaderboard_bucket_t* src_bucket = (const v1_rc_client_leaderboard_bucket_t*)&v1_leaderboard_list->buckets[0]; + const v1_rc_client_leaderboard_bucket_t* stop_bucket = src_bucket + v1_leaderboard_list->num_buckets; + rc_client_leaderboard_bucket_t* bucket; + uint32_t num_leaderboards = 0; + char* badge_url = NULL; + + new_list->info.public_.buckets = bucket = (rc_client_leaderboard_bucket_t*)calloc(v1_leaderboard_list->num_buckets, sizeof(*new_list->info.public_.buckets)); + if (!new_list->info.public_.buckets) + return (rc_client_leaderboard_list_t*)new_list; + + for (; src_bucket < stop_bucket; src_bucket++) + num_leaderboards += src_bucket->num_leaderboards; + + if (num_leaderboards) { + new_list->leaderboards = (rc_client_leaderboard_t*)calloc(num_leaderboards, sizeof(*new_list->leaderboards)); + new_list->leaderboards_pointers = (rc_client_leaderboard_t**)malloc(num_leaderboards * sizeof(rc_client_leaderboard_t*)); + if (!new_list->leaderboards || !new_list->leaderboards_pointers) + return (rc_client_leaderboard_list_t*)new_list; + } + + num_leaderboards = 0; + src_bucket = (const v1_rc_client_leaderboard_bucket_t*)&v1_leaderboard_list->buckets[0]; + for (; src_bucket < stop_bucket; src_bucket++, bucket++) { + memcpy(bucket, src_bucket, sizeof(*src_bucket)); + + if (src_bucket->num_leaderboards) { + const v1_rc_client_leaderboard_t** src_leaderboard = (const v1_rc_client_leaderboard_t**)src_bucket->leaderboards; + const v1_rc_client_leaderboard_t** stop_leaderboard = src_leaderboard + src_bucket->num_leaderboards; + rc_client_leaderboard_t** leaderboard = &new_list->leaderboards_pointers[num_leaderboards]; + + bucket->leaderboards = (const rc_client_leaderboard_t**)leaderboard; + + for (; src_leaderboard < stop_leaderboard; ++src_leaderboard, ++leaderboard) { + *leaderboard = &new_list->leaderboards[num_leaderboards++]; + memcpy(*leaderboard, *src_leaderboard, sizeof(**src_leaderboard)); + + (*leaderboard)->category = RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED; + } + } + } + + new_list->info.public_.num_buckets = v1_leaderboard_list->num_buckets; + } + + return (rc_client_leaderboard_list_t*)new_list; +} + #endif /* RC_CLIENT_SUPPORTS_EXTERNAL */ diff --git a/src/rc_client_external.h b/src/rc_client_external.h index b3c0e5eb..6237d9dd 100644 --- a/src/rc_client_external.h +++ b/src/rc_client_external.h @@ -52,6 +52,7 @@ typedef const rc_client_achievement_t* (RC_CCONV* rc_client_external_get_next_ac * and a destructor function. */ struct rc_client_leaderboard_list_info_t; typedef struct rc_client_leaderboard_list_info_t* (RC_CCONV *rc_client_external_create_leaderboard_list_func_t)(int grouping); +typedef struct rc_client_leaderboard_list_info_t* (RC_CCONV* rc_client_external_create_subset_leaderboard_list_func_t)(const rc_client_leaderboard_list_params_t* params); typedef const rc_client_leaderboard_t* (RC_CCONV *rc_client_external_get_leaderboard_info_func_t)(uint32_t id); /* NOTE: rc_client_external_begin_fetch_leaderboard_entries_func_t and rc_client_external_begin_fetch_leaderboard_entries_around_user_func_t @@ -156,12 +157,13 @@ typedef struct rc_client_external_t /* VERSION 7 */ rc_client_external_get_next_achievement_info_func_t get_next_achievement_info; -} rc_client_external_t; + /* VERSION 8 */ + rc_client_external_get_leaderboard_info_func_t get_leaderboard_info_v8; + rc_client_external_create_subset_leaderboard_list_func_t create_subset_leaderboard_list; -#define RC_CLIENT_EXTERNAL_VERSION 7 +} rc_client_external_t; -void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); -void rc_client_load_unknown_game(rc_client_t* client, const char* hash); +#define RC_CLIENT_EXTERNAL_VERSION 8 /* conversion support */ @@ -171,6 +173,8 @@ const rc_client_game_t* rc_client_external_convert_v1_game(const rc_client_t* cl const rc_client_subset_t* rc_client_external_convert_v1_subset(const rc_client_t* client, const rc_client_subset_t* v1_subset); const rc_client_achievement_t* rc_client_external_convert_v1_achievement(const rc_client_t* client, const rc_client_achievement_t* v1_achievement); rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(const rc_client_t* client, rc_client_achievement_list_t* v1_achievement_list); +const rc_client_leaderboard_t* rc_client_external_convert_v1_leaderboard(const rc_client_t* client, const rc_client_leaderboard_t* v1_leaderboard); +rc_client_leaderboard_list_t* rc_client_external_convert_v1_leaderboard_list(const rc_client_t* client, rc_client_leaderboard_list_t* v1_leaderboard_list); RC_END_C_DECLS diff --git a/src/rc_client_external_versions.h b/src/rc_client_external_versions.h index 90f6f689..841b5b54 100644 --- a/src/rc_client_external_versions.h +++ b/src/rc_client_external_versions.h @@ -144,6 +144,69 @@ typedef struct v3_rc_client_achievement_list_info_t { rc_client_destroy_achievement_list_func_t destroy_func; } v3_rc_client_achievement_list_info_t; +/* leaderboard */ + +typedef struct v1_rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t format; + uint8_t lower_is_better; +} v1_rc_client_leaderboard_t; + +typedef struct v8_rc_client_leaderboard_t { + const char* title; + const char* description; + const char* tracker_value; + uint32_t id; + uint8_t state; + uint8_t format; + uint8_t lower_is_better; + uint8_t category; +} v8_rc_client_leaderboard_t; + +/* leaderboard list */ + +typedef struct v1_rc_client_leaderboard_bucket_t { + const v1_rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v1_rc_client_leaderboard_bucket_t; + +typedef struct v1_rc_client_leaderboard_list_t { + const v1_rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} v1_rc_client_leaderboard_list_t; + +typedef struct v1_rc_client_leaderboard_list_info_t { + v1_rc_client_leaderboard_list_t public_; + rc_client_destroy_leaderboard_list_func_t destroy_func; +} v1_rc_client_leaderboard_list_info_t; + +typedef struct v8_rc_client_leaderboard_bucket_t { + const v8_rc_client_leaderboard_t** leaderboards; + uint32_t num_leaderboards; + + const char* label; + uint32_t subset_id; + uint8_t bucket_type; +} v8_rc_client_leaderboard_bucket_t; + +typedef struct v8_rc_client_leaderboard_list_t { + const v8_rc_client_leaderboard_bucket_t* buckets; + uint32_t num_buckets; +} v8_rc_client_leaderboard_list_t; + +typedef struct v8_rc_client_leaderboard_list_info_t { + v8_rc_client_leaderboard_list_t public_; + rc_client_destroy_leaderboard_list_func_t destroy_func; +} v8_rc_client_leaderboard_list_info_t; + /* user_game_summary */ typedef struct v1_rc_client_user_game_summary_t { diff --git a/src/rc_client_internal.h b/src/rc_client_internal.h index 3b04e830..a9cfaa6a 100644 --- a/src/rc_client_internal.h +++ b/src/rc_client_internal.h @@ -392,6 +392,8 @@ int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) #ifdef RC_CLIENT_SUPPORTS_HASH struct rc_hash_iterator; struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); +void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); +void rc_client_load_unknown_game(rc_client_t* client, const char* hash); #endif /* end helper functions for unit tests */ diff --git a/test/test_rc_client_external.c b/test/test_rc_client_external.c index 6c043b8e..dd9b6d2f 100644 --- a/test/test_rc_client_external.c +++ b/test/test_rc_client_external.c @@ -1411,7 +1411,6 @@ static void test_unload_game(void) static void test_v1_achievement_field_offsets(void) { - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, id); ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, description); ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, badge_name); ASSERT_FIELD_OFFSET(rc_client_achievement_t, v1_rc_client_achievement_t, measured_progress); @@ -1430,7 +1429,6 @@ static void test_v1_achievement_field_offsets(void) static void test_v3_achievement_field_offsets(void) { - ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, id); ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, description); ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, badge_name); ASSERT_FIELD_OFFSET(rc_client_achievement_t, v3_rc_client_achievement_t, measured_progress); @@ -1762,17 +1760,31 @@ static void test_create_achievement_list(void) /* ----- leaderboards ----- */ -typedef struct v1_rc_client_leaderboard_t { - const char* title; - const char* description; - const char* tracker_value; - uint32_t id; - uint8_t state; - uint8_t format; - uint8_t lower_is_better; -} v1_rc_client_leaderboard_t; -static const rc_client_leaderboard_t* rc_client_external_get_leaderboard_info(uint32_t id) +static void test_v1_leaderboard_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, title); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, description); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, tracker_value); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, id); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, state); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, format); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v1_rc_client_leaderboard_t, lower_is_better); +} + +static void test_v8_leaderboard_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, title); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, description); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, tracker_value); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, id); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, state); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, format); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, lower_is_better); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_t, v8_rc_client_leaderboard_t, category); +} + +static const rc_client_leaderboard_t* rc_client_external_get_leaderboard_info_v1(uint32_t id) { v1_rc_client_leaderboard_t* leaderboard = (v1_rc_client_leaderboard_t*) rc_buffer_alloc(&g_client->state.buffer, sizeof(v1_rc_client_leaderboard_t)); @@ -1789,24 +1801,23 @@ static const rc_client_leaderboard_t* rc_client_external_get_leaderboard_info(ui return (const rc_client_leaderboard_t*)leaderboard; } -typedef struct v1_rc_client_leaderboard_bucket_t { - rc_client_leaderboard_t** leaderboards; - uint32_t num_leaderboards; - - const char* label; - uint32_t subset_id; - uint8_t bucket_type; -} v1_rc_client_leaderboard_bucket_t; +static const rc_client_leaderboard_t* rc_client_external_get_leaderboard_info_v8(uint32_t id) +{ + v8_rc_client_leaderboard_t* leaderboard = (v8_rc_client_leaderboard_t*) + rc_buffer_alloc(&g_client->state.buffer, sizeof(v8_rc_client_leaderboard_t)); -typedef struct v1_rc_client_leaderboard_list_t { - v1_rc_client_leaderboard_bucket_t* buckets; - uint32_t num_buckets; -} v1_rc_client_leaderboard_list_t; + memset(leaderboard, 0, sizeof(*leaderboard)); + leaderboard->id = 1234; + leaderboard->title = "Leaderboard Title"; + leaderboard->description = "Do something cool"; + leaderboard->tracker_value = "000250"; + leaderboard->state = RC_CLIENT_LEADERBOARD_STATE_ACTIVE; + leaderboard->format = RC_CLIENT_LEADERBOARD_FORMAT_SCORE; + leaderboard->lower_is_better = 1; + leaderboard->category = RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED; -typedef struct v1_rc_client_leaderboard_list_info_t { - v1_rc_client_leaderboard_list_t public_; - rc_client_destroy_leaderboard_list_func_t destroy_func; -} v1_rc_client_leaderboard_list_info_t; + return (const rc_client_leaderboard_t*)leaderboard; +} static void assert_leaderboard_list_grouping(int grouping) { @@ -1819,20 +1830,154 @@ static void rc_client_external_destroy_leaderboard_list(rc_client_leaderboard_li free(list); } -static rc_client_leaderboard_list_info_t* rc_client_external_create_leaderboard_list(int grouping) +static void test_has_leaderboards(void) +{ + g_client = mock_client_with_external(); + g_client->state.external_client->has_leaderboards = rc_client_external_get_int; + + g_external_int = 0; + ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 0); + + g_external_int = 1; + ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 1); + + rc_client_destroy(g_client); +} + +static void test_get_leaderboard_info_v1(void) +{ + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info_v1; + + leaderboard = rc_client_get_leaderboard_info(g_client, 4); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->id, 1234); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); + ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); + ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + + rc_client_destroy(g_client); +} + +static void test_get_leaderboard_info_v8(void) +{ + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info_v8; + + leaderboard = rc_client_get_leaderboard_info(g_client, 4); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->id, 1234); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); + ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); + ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + ASSERT_NUM_EQUALS(leaderboard->category, RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED); + + rc_client_destroy(g_client); +} + +static void test_get_leaderboard_info(void) +{ + const rc_client_leaderboard_t* leaderboard; + + g_client = mock_client_with_external(); + g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info_v8; + + leaderboard = rc_client_get_leaderboard_info(g_client, 4); + ASSERT_PTR_NOT_NULL(leaderboard); + ASSERT_NUM_EQUALS(leaderboard->id, 1234); + ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); + ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); + ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); + ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); + ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); + ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + + rc_client_destroy(g_client); +} + +static void test_v1_leaderboard_list_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_info_t, v1_rc_client_leaderboard_list_info_t, public_); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_info_t, v1_rc_client_leaderboard_list_info_t, destroy_func); + + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_t, v1_rc_client_leaderboard_list_t, buckets); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_t, v1_rc_client_leaderboard_list_t, num_buckets); + + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v1_rc_client_leaderboard_bucket_t, leaderboards); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v1_rc_client_leaderboard_bucket_t, num_leaderboards); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v1_rc_client_leaderboard_bucket_t, label); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v1_rc_client_leaderboard_bucket_t, subset_id); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v1_rc_client_leaderboard_bucket_t, bucket_type); +} + +static void test_v8_leaderboard_list_field_offsets(void) +{ + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_info_t, v8_rc_client_leaderboard_list_info_t, public_); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_info_t, v8_rc_client_leaderboard_list_info_t, destroy_func); + + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_t, v8_rc_client_leaderboard_list_t, buckets); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_list_t, v8_rc_client_leaderboard_list_t, num_buckets); + + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v8_rc_client_leaderboard_bucket_t, leaderboards); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v8_rc_client_leaderboard_bucket_t, num_leaderboards); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v8_rc_client_leaderboard_bucket_t, label); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v8_rc_client_leaderboard_bucket_t, subset_id); + ASSERT_FIELD_OFFSET(rc_client_leaderboard_bucket_t, v8_rc_client_leaderboard_bucket_t, bucket_type); +} + +static rc_client_leaderboard_list_info_t* rc_client_external_create_leaderboard_list_v1(int grouping) { v1_rc_client_leaderboard_list_info_t* list; + v1_rc_client_leaderboard_bucket_t* bucket; assert_leaderboard_list_grouping(grouping); - list = (v1_rc_client_leaderboard_list_info_t*)calloc(1, sizeof(*list) + sizeof(v1_rc_client_leaderboard_bucket_t)); + list = (v1_rc_client_leaderboard_list_info_t*)calloc(1, sizeof(*list) + sizeof(v1_rc_client_leaderboard_bucket_t) + sizeof(v1_rc_client_leaderboard_t*) * 2); if (list) { list->public_.num_buckets = 1; - list->public_.buckets = (v1_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); - list->public_.buckets[0].num_leaderboards = 2; /* didn't actually allocate these */ - list->public_.buckets[0].bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; - list->public_.buckets[0].label = "Inactive"; - list->public_.buckets[0].subset_id = 1234; + list->public_.buckets = bucket = (v1_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); + bucket->num_leaderboards = 2; /* didn't actually allocate these */ + bucket->leaderboards = (v1_rc_client_leaderboard_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); + bucket->leaderboards[0] = (const v1_rc_client_leaderboard_t*)rc_client_external_get_achievement_info_v1(1234); + bucket->leaderboards[1] = (const v1_rc_client_leaderboard_t*)rc_client_external_get_achievement_info_v1(1235); + bucket->bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + bucket->label = "Inactive"; + bucket->subset_id = 1234; + + list->destroy_func = rc_client_external_destroy_leaderboard_list; + } + + return (rc_client_leaderboard_list_info_t*)list; +} + +static rc_client_leaderboard_list_info_t* rc_client_external_create_subset_leaderboard_list(const rc_client_leaderboard_list_params_t* params) +{ + v8_rc_client_leaderboard_list_info_t* list; + v8_rc_client_leaderboard_bucket_t* bucket; + + assert_leaderboard_list_grouping(params->grouping); + + list = (v8_rc_client_leaderboard_list_info_t*)calloc(1, sizeof(*list) + sizeof(v8_rc_client_leaderboard_bucket_t) + sizeof(v8_rc_client_leaderboard_t*) * 2); + if (list) { + list->public_.num_buckets = 1; + list->public_.buckets = bucket = (v8_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); + bucket->num_leaderboards = 2; /* didn't actually allocate these */ + bucket->leaderboards = (v8_rc_client_leaderboard_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); + bucket->leaderboards[0] = (const v8_rc_client_leaderboard_t*)rc_client_external_get_leaderboard_info_v8(1234); + bucket->leaderboards[1] = (const v8_rc_client_leaderboard_t*)rc_client_external_get_leaderboard_info_v8(1235); + bucket->bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; + bucket->label = "Inactive"; + bucket->subset_id = 1234; list->destroy_func = rc_client_external_destroy_leaderboard_list; } @@ -1845,7 +1990,8 @@ static void test_create_leaderboard_list(void) rc_client_leaderboard_list_t* list; g_client = mock_client_with_external(); - g_client->state.external_client->create_leaderboard_list = rc_client_external_create_leaderboard_list; + g_client->state.external_client->create_leaderboard_list = rc_client_external_create_leaderboard_list_v1; + g_client->state.external_client->create_subset_leaderboard_list = rc_client_external_create_subset_leaderboard_list; list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); ASSERT_PTR_NOT_NULL(list); @@ -1855,42 +2001,31 @@ static void test_create_leaderboard_list(void) ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].leaderboards[0]->category, RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED); rc_client_destroy_leaderboard_list(list); rc_client_destroy(g_client); } -static void test_has_leaderboards(void) -{ - g_client = mock_client_with_external(); - g_client->state.external_client->has_leaderboards = rc_client_external_get_int; - - g_external_int = 0; - ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 0); - - g_external_int = 1; - ASSERT_NUM_EQUALS(rc_client_has_leaderboards(g_client), 1); - - rc_client_destroy(g_client); -} - -static void test_get_leaderboard_info(void) +static void test_create_leaderboard_list_v1(void) { - const rc_client_leaderboard_t* leaderboard; + rc_client_leaderboard_list_t* list; g_client = mock_client_with_external(); - g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info; + g_client->state.external_client->create_leaderboard_list = rc_client_external_create_leaderboard_list_v1; - leaderboard = rc_client_get_leaderboard_info(g_client, 4); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->id, 1234); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); - ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); - ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); + list = rc_client_create_leaderboard_list(g_client, RC_CLIENT_LEADERBOARD_LIST_GROUPING_TRACKING); + ASSERT_PTR_NOT_NULL(list); + ASSERT_NUM_EQUALS(list->num_buckets, 1); + ASSERT_PTR_NOT_NULL(list->buckets); + ASSERT_NUM_EQUALS(list->buckets[0].num_leaderboards, 2); + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 1234); + ASSERT_STR_EQUALS(list->buckets[0].label, "Inactive"); + ASSERT_NUM_EQUALS(list->buckets[0].leaderboards[0]->category, RC_CLIENT_LEADERBOARD_CATEGORY_PROMOTED); + + rc_client_destroy_leaderboard_list(list); rc_client_destroy(g_client); } @@ -2171,10 +2306,17 @@ void test_client_external(void) { TEST(test_create_achievement_list); /* leaderboards */ - TEST(test_create_leaderboard_list); + TEST(test_v1_leaderboard_field_offsets); + TEST(test_v8_leaderboard_field_offsets); TEST(test_has_leaderboards); + TEST(test_get_leaderboard_info_v1); TEST(test_get_leaderboard_info); + TEST(test_v1_leaderboard_list_field_offsets); + TEST(test_v8_leaderboard_list_field_offsets); + TEST(test_create_leaderboard_list_v1); + TEST(test_create_leaderboard_list); + /* rich presence */ TEST(test_get_rich_presence_message); TEST(test_has_rich_presence); From 43d77e8974829d510dc16312819d2dc65b0555d3 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 3 Sep 2026 11:13:02 -0600 Subject: [PATCH 3/5] add rc_client_create_subset_achievement_list --- include/rc_client.h | 12 ++++ src/rc_client.c | 56 +++++++++++++----- src/rc_client_external.h | 4 +- test/test_rc_client.c | 120 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 16 deletions(-) diff --git a/include/rc_client.h b/include/rc_client.h index ab9cd949..c8f677ce 100644 --- a/include/rc_client.h +++ b/include/rc_client.h @@ -616,6 +616,18 @@ enum { */ RC_EXPORT rc_client_achievement_list_t* RC_CCONV rc_client_create_achievement_list(rc_client_t* client, int category, int grouping); +typedef struct rc_client_achievement_list_params_t { + uint32_t subset_id; + uint32_t grouping; + uint32_t category; +} rc_client_achievement_list_params_t; + +/** + * Creates a list of achievements matching the specified category and grouping. + * Returns an allocated list that must be free'd by calling rc_client_destroy_achievement_list. + */ +RC_EXPORT rc_client_achievement_list_t* RC_CCONV rc_client_create_subset_achievement_list(rc_client_t* client, const rc_client_achievement_list_params_t* params); + /** * Destroys a list allocated by rc_client_create_achievement_list. */ diff --git a/src/rc_client.c b/src/rc_client.c index 4273c156..2f406199 100644 --- a/src/rc_client.c +++ b/src/rc_client.c @@ -4245,7 +4245,30 @@ static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) return bucket; } +static int rc_client_is_subset_visible_in_achievement_list(const rc_client_subset_info_t* subset, const rc_client_achievement_list_params_t* params) +{ + if (!subset->active) + return 0; + + return (params->subset_id == 0 || params->subset_id == subset->public_.id); +} + +static int rc_client_is_achievement_visible_in_list(const rc_client_achievement_info_t* achievement, const rc_client_achievement_list_params_t* params) +{ + return (achievement->public_.category & params->category) != 0; +} + rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* client, int category, int grouping) +{ + rc_client_achievement_list_params_t params; + params.subset_id = 0; + params.grouping = grouping; + params.category = category; + + return rc_client_create_subset_achievement_list(client, ¶ms); +} + +rc_client_achievement_list_t* rc_client_create_subset_achievement_list(rc_client_t* client, const rc_client_achievement_list_params_t* params) { rc_client_achievement_info_t* achievement; rc_client_achievement_info_t* stop; @@ -4281,12 +4304,15 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli #ifdef RC_CLIENT_SUPPORTS_EXTERNAL if (client->state.external_client) { + if (client->state.external_client->create_subset_achievement_list) + return (rc_client_achievement_list_t*)client->state.external_client->create_subset_achievement_list(params); + if (client->state.external_client->create_achievement_list_v3) - return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list_v3(category, grouping); + return (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list_v3(params->category, params->grouping); if (client->state.external_client->create_achievement_list) return rc_client_external_convert_v1_achievement_list(client, - (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(category, grouping)); + (rc_client_achievement_list_t*)client->state.external_client->create_achievement_list(params->category, params->grouping)); } #endif @@ -4299,16 +4325,16 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli subset = client->game->subsets; for (; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_achievement_list(subset, params)) continue; num_subsets++; achievement = subset->achievements; stop = achievement + subset->public_.num_achievements; for (; achievement < stop; ++achievement) { - if (achievement->public_.category & category) { + if (rc_client_is_achievement_visible_in_list(achievement, params)) { rc_client_update_achievement_display_information(client, achievement, recent_unlock_time); - bucket_counts[rc_client_map_bucket(achievement->public_.bucket, grouping)]++; + bucket_counts[rc_client_map_bucket(achievement->public_.bucket, params->grouping)]++; } } } @@ -4337,14 +4363,14 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli subset = client->game->subsets; for (; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_achievement_list(subset, params)) 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) { + if (rc_client_is_achievement_visible_in_list(achievement, params)) { + if (rc_client_map_bucket(achievement->public_.bucket, params->grouping) == i) { ++num_buckets; break; } @@ -4360,7 +4386,7 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli list->public_.buckets = bucket_ptr = (rc_client_achievement_bucket_t*)((uint8_t*)list + list_size); achievement_ptr = (const rc_client_achievement_t**)((uint8_t*)bucket_ptr + buckets_size); - if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS) { + if (params->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]) @@ -4368,14 +4394,14 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli bucket_achievements = achievement_ptr; for (subset = client->game->subsets; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_achievement_list(subset, params)) 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) { + if (rc_client_is_achievement_visible_in_list(achievement, params) && + rc_client_map_bucket(achievement->public_.bucket, params->grouping) == bucket_type) { *achievement_ptr++ = &achievement->public_; } } @@ -4399,7 +4425,7 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli } for (subset = client->game->subsets; subset; subset = subset->next) { - if (!subset->active) + if (!rc_client_is_subset_visible_in_achievement_list(subset, params)) continue; for (i = 0; i < sizeof(subset_bucket_order) / sizeof(subset_bucket_order[0]); ++i) { @@ -4412,8 +4438,8 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli 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) { + if (rc_client_is_achievement_visible_in_list(achievement, params) && + rc_client_map_bucket(achievement->public_.bucket, params->grouping) == bucket_type) { *achievement_ptr++ = &achievement->public_; } } diff --git a/src/rc_client_external.h b/src/rc_client_external.h index 6237d9dd..2391c5e5 100644 --- a/src/rc_client_external.h +++ b/src/rc_client_external.h @@ -44,7 +44,8 @@ typedef void (RC_CCONV* rc_client_external_add_game_hash_func_t)(const char* has /* NOTE: rc_client_external_create_achievement_list_func_t returns an internal wrapper structure which contains the public list * and a destructor function. */ struct rc_client_achievement_list_info_t; -typedef struct rc_client_achievement_list_info_t* (RC_CCONV *rc_client_external_create_achievement_list_func_t)(int category, int grouping); +typedef struct rc_client_achievement_list_info_t* (RC_CCONV* rc_client_external_create_achievement_list_func_t)(int category, int grouping); +typedef struct rc_client_achievement_list_info_t* (RC_CCONV* rc_client_external_create_subset_achievement_list_func_t)(const rc_client_achievement_list_params_t* params); typedef const rc_client_achievement_t* (RC_CCONV *rc_client_external_get_achievement_info_func_t)(uint32_t id); typedef const rc_client_achievement_t* (RC_CCONV* rc_client_external_get_next_achievement_info_func_t)(uint32_t id, int grouping); @@ -160,6 +161,7 @@ typedef struct rc_client_external_t /* VERSION 8 */ rc_client_external_get_leaderboard_info_func_t get_leaderboard_info_v8; rc_client_external_create_subset_leaderboard_list_func_t create_subset_leaderboard_list; + rc_client_external_create_subset_achievement_list_func_t create_subset_achievement_list; } rc_client_external_t; diff --git a/test/test_rc_client.c b/test/test_rc_client.c index 776ec6c0..5631e99c 100644 --- a/test/test_rc_client.c +++ b/test/test_rc_client.c @@ -5370,6 +5370,125 @@ static void test_achievement_list_subset_buckets(void) rc_client_destroy(g_client); } +static void test_achievement_list_single_subset_buckets(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_list_params_t params; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_subset, unlock_8_and_5502); + mock_memory(memory, sizeof(memory)); + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b", + "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + mock_api_response("r=awardachievement&u=Username&t=ApiToken&a=5501&h=1&m=0123456789ABCDEF&v=9b9bdf5501eb6289a6655affbcc695e6", + "{\"Success\":true,\"Score\":5437,\"SoftcoreScore\":777,\"AchievementID\":5,\"AchievementsRemaining\":6}"); + + memset(¶ms, 0, sizeof(params)); + params.subset_id = 2345; + params.category = RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE; + params.grouping = RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS; + + list = rc_client_create_subset_achievement_list(g_client, ¶ms); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[1]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + memory[5] = 5; /* trigger achievement 5 */ + memory[6] = 2; /* start measuring achievement 6 */ + memory[1] = 1; /* begin challenge achievement 7 */ + memory[0x11] = 100; /* start measuring achievements 70 and 71 */ + memory[0x17] = 7; /* trigger achievement 5501 */ + rc_client_do_frame(g_client); + event_count = 0; + + /* set the unlock time for achievement 5 back one second to ensure consistent sorting */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time--; + + list = rc_client_create_subset_achievement_list(g_client, ¶ms); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + + /* achievement 7 is an active challenge, but not part of the selected subset */ + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5501); + /* achievement 5 also recently unlocked, but not part of the selected subset */ + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + /* recently unlocked achievements no longer recent */ + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5))->unlock_time -= 15 * 60; + ((rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5501))->unlock_time -= 15 * 60; + memory[6] = 5; /* almost there achievement 6 */ + memory[1] = 0; /* stop challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + list = rc_client_create_subset_achievement_list(g_client, ¶ms); + ASSERT_PTR_NOT_NULL(list); + if (list) { + ASSERT_NUM_EQUALS(list->num_buckets, 2); + + /* achievement 6 is "almost there", but not part of the subset */ + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 5503); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 2); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5501); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[1]->id, 5502); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + static void test_get_next_achievement_first(void) { const rc_client_achievement_t* achievement; @@ -10699,6 +10818,7 @@ void test_client(void) { TEST(test_achievement_list_buckets_with_unsynced); TEST(test_achievement_list_subset_with_unofficial_and_unsupported); TEST(test_achievement_list_subset_buckets); + TEST(test_achievement_list_single_subset_buckets); TEST(test_get_next_achievement_first); TEST(test_get_next_achievement_sequence); From 4574aa6b5912058139f477f06e97d082bbfd3e6a Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 3 Sep 2026 14:07:54 -0600 Subject: [PATCH 4/5] remove unused variables --- src/rc_client_external.c | 3 --- src/rc_client_internal.h | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/rc_client_external.c b/src/rc_client_external.c index 49539533..dfbdf0a0 100644 --- a/src/rc_client_external.c +++ b/src/rc_client_external.c @@ -284,8 +284,6 @@ rc_client_achievement_list_t* rc_client_external_convert_v1_achievement_list(con const rc_client_leaderboard_t* rc_client_external_convert_v1_leaderboard(const rc_client_t* client, const rc_client_leaderboard_t* v1_leaderboard) { rc_client_leaderboard_t* converted = NULL; - char* badge_url = NULL; - char* badge_locked_url = NULL; const uint32_t num_leaderboards = sizeof(client->state.external_client_conversions->leaderboards) / sizeof(client->state.external_client_conversions->leaderboards[0]); uint32_t index; @@ -358,7 +356,6 @@ rc_client_leaderboard_list_t* rc_client_external_convert_v1_leaderboard_list(con const v1_rc_client_leaderboard_bucket_t* stop_bucket = src_bucket + v1_leaderboard_list->num_buckets; rc_client_leaderboard_bucket_t* bucket; uint32_t num_leaderboards = 0; - char* badge_url = NULL; new_list->info.public_.buckets = bucket = (rc_client_leaderboard_bucket_t*)calloc(v1_leaderboard_list->num_buckets, sizeof(*new_list->info.public_.buckets)); if (!new_list->info.public_.buckets) diff --git a/src/rc_client_internal.h b/src/rc_client_internal.h index a9cfaa6a..c76de9b8 100644 --- a/src/rc_client_internal.h +++ b/src/rc_client_internal.h @@ -393,8 +393,8 @@ int rc_value_contains_memref(const rc_value_t* value, const rc_memref_t* memref) struct rc_hash_iterator; struct rc_hash_iterator* rc_client_get_load_state_hash_iterator(rc_client_t* client); void rc_client_add_game_hash(rc_client_t* client, const char* hash, uint32_t game_id); -void rc_client_load_unknown_game(rc_client_t* client, const char* hash); #endif +void rc_client_load_unknown_game(rc_client_t* client, const char* hash); /* end helper functions for unit tests */ enum { From 30132c5baa0da6b837dc40187a862966369797d8 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 3 Sep 2026 14:11:05 -0600 Subject: [PATCH 5/5] remove unused function --- test/test_rc_client_external.c | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/test/test_rc_client_external.c b/test/test_rc_client_external.c index dd9b6d2f..7e5f315f 100644 --- a/test/test_rc_client_external.c +++ b/test/test_rc_client_external.c @@ -1864,27 +1864,6 @@ static void test_get_leaderboard_info_v1(void) rc_client_destroy(g_client); } -static void test_get_leaderboard_info_v8(void) -{ - const rc_client_leaderboard_t* leaderboard; - - g_client = mock_client_with_external(); - g_client->state.external_client->get_leaderboard_info = rc_client_external_get_leaderboard_info_v8; - - leaderboard = rc_client_get_leaderboard_info(g_client, 4); - ASSERT_PTR_NOT_NULL(leaderboard); - ASSERT_NUM_EQUALS(leaderboard->id, 1234); - ASSERT_STR_EQUALS(leaderboard->title, "Leaderboard Title"); - ASSERT_STR_EQUALS(leaderboard->description, "Do something cool"); - ASSERT_STR_EQUALS(leaderboard->tracker_value, "000250"); - ASSERT_NUM_EQUALS(leaderboard->state, RC_CLIENT_LEADERBOARD_STATE_ACTIVE); - ASSERT_NUM_EQUALS(leaderboard->format, RC_CLIENT_LEADERBOARD_FORMAT_SCORE); - ASSERT_NUM_EQUALS(leaderboard->lower_is_better, 1); - ASSERT_NUM_EQUALS(leaderboard->category, RC_CLIENT_LEADERBOARD_CATEGORY_UNPROMOTED); - - rc_client_destroy(g_client); -} - static void test_get_leaderboard_info(void) { const rc_client_leaderboard_t* leaderboard; @@ -1947,7 +1926,7 @@ static rc_client_leaderboard_list_info_t* rc_client_external_create_leaderboard_ list->public_.num_buckets = 1; list->public_.buckets = bucket = (v1_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); bucket->num_leaderboards = 2; /* didn't actually allocate these */ - bucket->leaderboards = (v1_rc_client_leaderboard_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); + bucket->leaderboards = (const v1_rc_client_leaderboard_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); bucket->leaderboards[0] = (const v1_rc_client_leaderboard_t*)rc_client_external_get_achievement_info_v1(1234); bucket->leaderboards[1] = (const v1_rc_client_leaderboard_t*)rc_client_external_get_achievement_info_v1(1235); bucket->bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE; @@ -1972,7 +1951,7 @@ static rc_client_leaderboard_list_info_t* rc_client_external_create_subset_leade list->public_.num_buckets = 1; list->public_.buckets = bucket = (v8_rc_client_leaderboard_bucket_t*)((uint8_t*)list + sizeof(*list)); bucket->num_leaderboards = 2; /* didn't actually allocate these */ - bucket->leaderboards = (v8_rc_client_leaderboard_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); + bucket->leaderboards = (const v8_rc_client_leaderboard_t**)((uint8_t*)list->public_.buckets + sizeof(*list->public_.buckets)); bucket->leaderboards[0] = (const v8_rc_client_leaderboard_t*)rc_client_external_get_leaderboard_info_v8(1234); bucket->leaderboards[1] = (const v8_rc_client_leaderboard_t*)rc_client_external_get_leaderboard_info_v8(1235); bucket->bucket_type = RC_CLIENT_LEADERBOARD_BUCKET_INACTIVE;