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