diff --git a/include/rc_client.h b/include/rc_client.h index 1e75c724..73b9475c 100644 --- a/include/rc_client.h +++ b/include/rc_client.h @@ -312,7 +312,8 @@ enum { RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED = 5, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE = 6, RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE = 7, - NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 8 + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED = 8, + NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS = 9 }; enum { diff --git a/src/rc_client.c b/src/rc_client.c index ebfbf368..5c1d1206 100644 --- a/src/rc_client.c +++ b/src/rc_client.c @@ -79,6 +79,7 @@ static void rc_client_raise_leaderboard_events(rc_client_t* client, rc_client_su static void rc_client_raise_pending_events(rc_client_t* client, rc_client_game_info_t* game); static void rc_client_reschedule_callback(rc_client_t* client, rc_client_scheduled_callback_data_t* callback, rc_clock_t when); static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); +static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id); static void rc_client_submit_leaderboard_entry_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now); /* ===== Construction/Destruction ===== */ @@ -2738,7 +2739,14 @@ static void rc_client_update_achievement_display_information(rc_client_t* client if (achievement->public_.state == RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED) { /* achievement unlocked */ - new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + if (achievement->public_.unlock_time >= recent_unlock_time) { + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; + } else { + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; + + if (client->state.disconnect && rc_client_is_award_achievement_pending(client, achievement->public_.id)) + new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED; + } } else { /* active achievement */ @@ -2779,9 +2787,6 @@ static void rc_client_update_achievement_display_information(rc_client_t* client } } - if (new_bucket == RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED && achievement->public_.unlock_time >= recent_unlock_time) - new_bucket = RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED; - achievement->public_.bucket = new_bucket; } @@ -2795,6 +2800,7 @@ static const char* rc_client_get_achievement_bucket_label(uint8_t bucket_type) case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: return "Recently Unlocked"; case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: return "Active Challenges"; case RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE: return "Almost There"; + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: return "Unlocks Not Synced to Server"; default: return "Unknown"; } } @@ -2852,6 +2858,7 @@ static uint8_t rc_client_map_bucket(uint8_t bucket, int grouping) if (grouping == RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE) { switch (bucket) { case RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED: + case RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED: return RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED; case RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE: @@ -2876,7 +2883,7 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli rc_client_achievement_list_info_t* list; rc_client_subset_info_t* subset; const uint32_t list_size = RC_ALIGN(sizeof(*list)); - uint32_t bucket_counts[16]; + uint32_t bucket_counts[NUM_RC_CLIENT_ACHIEVEMENT_BUCKETS]; uint32_t num_buckets; uint32_t num_achievements; size_t buckets_size; @@ -2886,7 +2893,8 @@ rc_client_achievement_list_t* rc_client_create_achievement_list(rc_client_t* cli const uint8_t shared_bucket_order[] = { RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED, - RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE + RC_CLIENT_ACHIEVEMENT_BUCKET_ALMOST_THERE, + RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED, }; const uint8_t subset_bucket_order[] = { RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED, @@ -3165,6 +3173,24 @@ typedef struct rc_client_award_achievement_callback_data_t rc_client_scheduled_callback_data_t* scheduled_callback_data; } rc_client_award_achievement_callback_data_t; +static int rc_client_is_award_achievement_pending(const rc_client_t* client, uint32_t achievement_id) +{ + /* assume lock already held */ + rc_client_scheduled_callback_data_t* scheduled_callback = client->state.scheduled_callbacks; + for (; scheduled_callback; scheduled_callback = scheduled_callback->next) + { + if (scheduled_callback->callback == rc_client_award_achievement_retry) + { + rc_client_award_achievement_callback_data_t* ach_data = + (rc_client_award_achievement_callback_data_t*)scheduled_callback->data; + if (ach_data->id == achievement_id) + return 1; + } + } + + return 0; +} + static void rc_client_award_achievement_server_call(rc_client_award_achievement_callback_data_t* ach_data); static void rc_client_award_achievement_retry(rc_client_scheduled_callback_data_t* callback_data, rc_client_t* client, rc_clock_t now) @@ -5531,7 +5557,7 @@ void rc_client_set_host(const rc_client_t* client, const char* hostname) rc_api_set_host(hostname); #ifdef RC_CLIENT_SUPPORTS_EXTERNAL - if (client->state.external_client && client->state.external_client->set_host) + if (client && client->state.external_client && client->state.external_client->set_host) client->state.external_client->set_host(hostname); #endif } diff --git a/test/test_rc_client.c b/test/test_rc_client.c index 61790687..8b279b1a 100644 --- a/test/test_rc_client.c +++ b/test/test_rc_client.c @@ -3446,6 +3446,7 @@ static void test_achievement_list_buckets(void) rc_client_destroy_achievement_list(list); } + /* also check mapping to lock state */ list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_LOCK_STATE); ASSERT_PTR_NOT_NULL(list); if (list) { @@ -3650,6 +3651,147 @@ static void test_achievement_list_buckets_progress_sort_big_ids(void) rc_client_destroy(g_client); } +static void test_achievement_list_buckets_with_unsynced(void) +{ + rc_client_achievement_list_t* list; + rc_client_achievement_t* achievement; + const char* unlock_request_params = "r=awardachievement&u=Username&t=ApiToken&a=5&h=1&m=0123456789ABCDEF&v=732f8e30e9c1eb08948dda098c305d8b"; + + uint8_t memory[64]; + memset(memory, 0, sizeof(memory)); + + g_client = mock_client_game_loaded(patchdata_exhaustive, unlock_8); + g_client->callbacks.server_call = rc_client_server_call_async; + mock_memory(memory, sizeof(memory)); + + /* discard the queued ping to make finding the retry easier */ + g_client->state.scheduled_callbacks = NULL; + + rc_client_do_frame(g_client); /* advance achievements out of waiting state */ + event_count = 0; + + memory[5] = 5; /* trigger achievement 5 */ + memory[1] = 1; /* begin challenge achievement 7 */ + rc_client_do_frame(g_client); + event_count = 0; + + /* first failure will immediately requeue the request */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_pending(unlock_request_params); + ASSERT_PTR_NULL(g_client->state.scheduled_callbacks); + rc_client_idle(g_client); + + /* second failure will queue it */ + async_api_error(unlock_request_params, response_503, 503); + assert_api_call_count(unlock_request_params, 0); + ASSERT_PTR_NOT_NULL(g_client->state.scheduled_callbacks); + rc_client_idle(g_client); + event_count = 0; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) + { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_RECENTLY_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Recently Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* adjust unlock time on achievement 5 so it's no longer recent */ + achievement = (rc_client_achievement_t*)rc_client_get_achievement_info(g_client, 5); + achievement->unlock_time -= 15 * 60; + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) + { + ASSERT_NUM_EQUALS(list->num_buckets, 4); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + ASSERT_NUM_EQUALS(list->buckets[1].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNSYNCED); + ASSERT_NUM_EQUALS(list->buckets[1].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[1].label, "Unlocks Not Synced to Server"); + ASSERT_NUM_EQUALS(list->buckets[1].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[1].achievements[0]->id, 5); + + ASSERT_NUM_EQUALS(list->buckets[2].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_LOCKED); + ASSERT_NUM_EQUALS(list->buckets[2].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[2].label, "Locked"); + ASSERT_NUM_EQUALS(list->buckets[2].num_achievements, 4); + + ASSERT_NUM_EQUALS(list->buckets[3].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_UNLOCKED); + ASSERT_NUM_EQUALS(list->buckets[3].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[3].label, "Unlocked"); + ASSERT_NUM_EQUALS(list->buckets[3].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[3].achievements[0]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + /* allow unlock request to succeed */ + g_now += 2 * 1000; + rc_client_idle(g_client); + assert_api_pending(unlock_request_params); + async_api_response(unlock_request_params, "{\"Success\":true,\"Score\":5432,\"SoftcoreScore\":777,\"AchievementID\":8,\"AchievementsRemaining\":11}"); + + list = rc_client_create_achievement_list(g_client, RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE, RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS); + ASSERT_PTR_NOT_NULL(list); + if (list) + { + ASSERT_NUM_EQUALS(list->num_buckets, 3); + + ASSERT_NUM_EQUALS(list->buckets[0].bucket_type, RC_CLIENT_ACHIEVEMENT_BUCKET_ACTIVE_CHALLENGE); + ASSERT_NUM_EQUALS(list->buckets[0].subset_id, 0); + ASSERT_STR_EQUALS(list->buckets[0].label, "Active Challenges"); + ASSERT_NUM_EQUALS(list->buckets[0].num_achievements, 1); + ASSERT_NUM_EQUALS(list->buckets[0].achievements[0]->id, 7); + + 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, 4); + + 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, 2); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[0]->id, 5); + ASSERT_NUM_EQUALS(list->buckets[2].achievements[1]->id, 8); + + rc_client_destroy_achievement_list(list); + } + + rc_client_destroy(g_client); +} + static void test_achievement_list_subset_with_unofficial_and_unsupported(void) { rc_client_achievement_list_t* list; @@ -8341,6 +8483,7 @@ void test_client(void) { TEST(test_achievement_list_buckets); TEST(test_achievement_list_buckets_progress_sort); TEST(test_achievement_list_buckets_progress_sort_big_ids); + 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_subset_buckets_subset_first);