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