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