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