Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/dep/rcheevos/src/rapi/rc_api_runtime.c
4806 views
1
#include "rc_api_runtime.h"
2
#include "rc_api_common.h"
3
4
#include "rc_runtime.h"
5
#include "rc_runtime_types.h"
6
#include "../rc_compat.h"
7
#include "../rhash/md5.h"
8
9
#include <stdlib.h>
10
#include <stdio.h>
11
#include <string.h>
12
13
/* --- Resolve Hash --- */
14
15
int rc_api_init_resolve_hash_request(rc_api_request_t* request, const rc_api_resolve_hash_request_t* api_params) {
16
return rc_api_init_resolve_hash_request_hosted(request, api_params, &g_host);
17
}
18
19
int rc_api_init_resolve_hash_request_hosted(rc_api_request_t* request,
20
const rc_api_resolve_hash_request_t* api_params,
21
const rc_api_host_t* host) {
22
rc_api_url_builder_t builder;
23
24
rc_api_url_build_dorequest_url(request, host);
25
26
if (!api_params->game_hash || !*api_params->game_hash)
27
return RC_INVALID_STATE;
28
29
rc_url_builder_init(&builder, &request->buffer, 48);
30
rc_url_builder_append_str_param(&builder, "r", "gameid");
31
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
32
request->post_data = rc_url_builder_finalize(&builder);
33
request->content_type = RC_CONTENT_TYPE_URLENCODED;
34
35
return builder.result;
36
}
37
38
int rc_api_process_resolve_hash_response(rc_api_resolve_hash_response_t* response, const char* server_response) {
39
rc_api_server_response_t response_obj;
40
41
memset(&response_obj, 0, sizeof(response_obj));
42
response_obj.body = server_response;
43
response_obj.body_length = rc_json_get_object_string_length(server_response);
44
45
return rc_api_process_resolve_hash_server_response(response, &response_obj);
46
}
47
48
int rc_api_process_resolve_hash_server_response(rc_api_resolve_hash_response_t* response, const rc_api_server_response_t* server_response) {
49
int result;
50
rc_json_field_t fields[] = {
51
RC_JSON_NEW_FIELD("Success"),
52
RC_JSON_NEW_FIELD("Error"),
53
RC_JSON_NEW_FIELD("GameID")
54
};
55
56
memset(response, 0, sizeof(*response));
57
rc_buffer_init(&response->response.buffer);
58
59
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
60
if (result != RC_OK)
61
return result;
62
63
rc_json_get_required_unum(&response->game_id, &response->response, &fields[2], "GameID");
64
return RC_OK;
65
}
66
67
void rc_api_destroy_resolve_hash_response(rc_api_resolve_hash_response_t* response) {
68
rc_buffer_destroy(&response->response.buffer);
69
}
70
71
/* --- Fetch Game Data --- */
72
73
int rc_api_init_fetch_game_data_request(rc_api_request_t* request, const rc_api_fetch_game_data_request_t* api_params) {
74
return rc_api_init_fetch_game_data_request_hosted(request, api_params, &g_host);
75
}
76
77
int rc_api_init_fetch_game_data_request_hosted(rc_api_request_t* request,
78
const rc_api_fetch_game_data_request_t* api_params,
79
const rc_api_host_t* host) {
80
rc_api_url_builder_t builder;
81
82
rc_api_url_build_dorequest_url(request, host);
83
84
if (api_params->game_id == 0 && (!api_params->game_hash || !api_params->game_hash[0]))
85
return RC_INVALID_STATE;
86
87
rc_url_builder_init(&builder, &request->buffer, 48);
88
if (rc_api_url_build_dorequest(&builder, "patch", api_params->username, api_params->api_token)) {
89
if (api_params->game_id)
90
rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
91
else
92
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
93
94
request->post_data = rc_url_builder_finalize(&builder);
95
request->content_type = RC_CONTENT_TYPE_URLENCODED;
96
}
97
98
return builder.result;
99
}
100
101
int rc_api_process_fetch_game_data_response(rc_api_fetch_game_data_response_t* response, const char* server_response) {
102
rc_api_server_response_t response_obj;
103
104
memset(&response_obj, 0, sizeof(response_obj));
105
response_obj.body = server_response;
106
response_obj.body_length = rc_json_get_object_string_length(server_response);
107
108
return rc_api_process_fetch_game_data_server_response(response, &response_obj);
109
}
110
111
static int rc_api_process_fetch_game_data_achievements(rc_api_response_t* response, rc_api_achievement_definition_t* achievement, rc_json_field_t* array_field) {
112
rc_json_iterator_t iterator;
113
const char* last_author = "";
114
const char* last_author_field = "";
115
size_t last_author_len = 0;
116
uint32_t timet;
117
size_t len;
118
119
rc_json_field_t achievement_fields[] = {
120
RC_JSON_NEW_FIELD("ID"),
121
RC_JSON_NEW_FIELD("Title"),
122
RC_JSON_NEW_FIELD("Description"),
123
RC_JSON_NEW_FIELD("Flags"),
124
RC_JSON_NEW_FIELD("Points"),
125
RC_JSON_NEW_FIELD("MemAddr"),
126
RC_JSON_NEW_FIELD("Author"),
127
RC_JSON_NEW_FIELD("BadgeName"),
128
RC_JSON_NEW_FIELD("Created"),
129
RC_JSON_NEW_FIELD("Modified"),
130
RC_JSON_NEW_FIELD("Type"),
131
RC_JSON_NEW_FIELD("Rarity"),
132
RC_JSON_NEW_FIELD("RarityHardcore"),
133
RC_JSON_NEW_FIELD("BadgeURL"),
134
RC_JSON_NEW_FIELD("BadgeLockedURL")
135
};
136
137
memset(&iterator, 0, sizeof(iterator));
138
iterator.json = array_field->value_start;
139
iterator.end = array_field->value_end;
140
141
while (rc_json_get_array_entry_object(achievement_fields, sizeof(achievement_fields) / sizeof(achievement_fields[0]), &iterator)) {
142
if (!rc_json_get_required_unum(&achievement->id, response, &achievement_fields[0], "ID"))
143
return RC_MISSING_VALUE;
144
if (!rc_json_get_required_string(&achievement->title, response, &achievement_fields[1], "Title"))
145
return RC_MISSING_VALUE;
146
if (!rc_json_get_required_string(&achievement->description, response, &achievement_fields[2], "Description"))
147
return RC_MISSING_VALUE;
148
if (!rc_json_get_required_unum(&achievement->category, response, &achievement_fields[3], "Flags"))
149
return RC_MISSING_VALUE;
150
if (!rc_json_get_required_unum(&achievement->points, response, &achievement_fields[4], "Points"))
151
return RC_MISSING_VALUE;
152
if (!rc_json_get_required_string(&achievement->definition, response, &achievement_fields[5], "MemAddr"))
153
return RC_MISSING_VALUE;
154
if (!rc_json_get_required_string(&achievement->badge_name, response, &achievement_fields[7], "BadgeName"))
155
return RC_MISSING_VALUE;
156
157
rc_json_get_optional_string(&achievement->badge_url, response, &achievement_fields[13], "BadgeURL", "");
158
if (!achievement->badge_url[0])
159
achievement->badge_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT, achievement->badge_name);
160
161
rc_json_get_optional_string(&achievement->badge_locked_url, response, &achievement_fields[14], "BadgeLockedURL", "");
162
if (!achievement->badge_locked_url[0])
163
achievement->badge_locked_url = rc_api_build_avatar_url(&response->buffer, RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, achievement->badge_name);
164
165
len = achievement_fields[6].value_end - achievement_fields[6].value_start;
166
if (len == last_author_len && memcmp(achievement_fields[6].value_start, last_author_field, len) == 0) {
167
achievement->author = last_author;
168
}
169
else {
170
if (!rc_json_get_required_string(&achievement->author, response, &achievement_fields[6], "Author"))
171
return RC_MISSING_VALUE;
172
173
if (achievement->author == NULL) {
174
/* ensure we don't pass NULL out to client */
175
last_author = achievement->author = "";
176
last_author_len = 0;
177
} else {
178
last_author = achievement->author;
179
last_author_field = achievement_fields[6].value_start;
180
last_author_len = len;
181
}
182
}
183
184
if (!rc_json_get_required_unum(&timet, response, &achievement_fields[8], "Created"))
185
return RC_MISSING_VALUE;
186
achievement->created = (time_t)timet;
187
if (!rc_json_get_required_unum(&timet, response, &achievement_fields[9], "Modified"))
188
return RC_MISSING_VALUE;
189
achievement->updated = (time_t)timet;
190
191
if (rc_json_field_string_matches(&achievement_fields[10], ""))
192
achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;
193
else if (rc_json_field_string_matches(&achievement_fields[10], "progression"))
194
achievement->type = RC_ACHIEVEMENT_TYPE_PROGRESSION;
195
else if (rc_json_field_string_matches(&achievement_fields[10], "missable"))
196
achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
197
else if (rc_json_field_string_matches(&achievement_fields[10], "win_condition"))
198
achievement->type = RC_ACHIEVEMENT_TYPE_WIN;
199
else
200
achievement->type = RC_ACHIEVEMENT_TYPE_STANDARD;
201
202
/* legacy support : if title contains[m], change type to missable and remove[m] from title */
203
if (memcmp(achievement->title, "[m]", 3) == 0) {
204
len = 3;
205
while (achievement->title[len] == ' ')
206
++len;
207
achievement->title += len;
208
achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
209
}
210
else if (achievement_fields[1].value_end && memcmp(achievement_fields[1].value_end - 4, "[m]", 3) == 0) {
211
len = strlen(achievement->title) - 3;
212
while (achievement->title[len - 1] == ' ')
213
--len;
214
((char*)achievement->title)[len] = '\0';
215
achievement->type = RC_ACHIEVEMENT_TYPE_MISSABLE;
216
}
217
218
rc_json_get_optional_float(&achievement->rarity, &achievement_fields[11], "Rarity", 100.0);
219
rc_json_get_optional_float(&achievement->rarity_hardcore, &achievement_fields[12], "RarityHardcore", 100.0);
220
221
++achievement;
222
}
223
224
return RC_OK;
225
}
226
227
static int rc_api_process_fetch_game_data_leaderboards(rc_api_response_t* response, rc_api_leaderboard_definition_t* leaderboard, rc_json_field_t* array_field) {
228
rc_json_iterator_t iterator;
229
size_t len;
230
int result;
231
char format[16];
232
233
rc_json_field_t leaderboard_fields[] = {
234
RC_JSON_NEW_FIELD("ID"),
235
RC_JSON_NEW_FIELD("Title"),
236
RC_JSON_NEW_FIELD("Description"),
237
RC_JSON_NEW_FIELD("Mem"),
238
RC_JSON_NEW_FIELD("Format"),
239
RC_JSON_NEW_FIELD("LowerIsBetter"),
240
RC_JSON_NEW_FIELD("Hidden")
241
};
242
243
memset(&iterator, 0, sizeof(iterator));
244
iterator.json = array_field->value_start;
245
iterator.end = array_field->value_end;
246
247
while (rc_json_get_array_entry_object(leaderboard_fields, sizeof(leaderboard_fields) / sizeof(leaderboard_fields[0]), &iterator)) {
248
if (!rc_json_get_required_unum(&leaderboard->id, response, &leaderboard_fields[0], "ID"))
249
return RC_MISSING_VALUE;
250
if (!rc_json_get_required_string(&leaderboard->title, response, &leaderboard_fields[1], "Title"))
251
return RC_MISSING_VALUE;
252
if (!rc_json_get_required_string(&leaderboard->description, response, &leaderboard_fields[2], "Description"))
253
return RC_MISSING_VALUE;
254
if (!rc_json_get_required_string(&leaderboard->definition, response, &leaderboard_fields[3], "Mem"))
255
return RC_MISSING_VALUE;
256
rc_json_get_optional_bool(&result, &leaderboard_fields[5], "LowerIsBetter", 0);
257
leaderboard->lower_is_better = (uint8_t)result;
258
rc_json_get_optional_bool(&result, &leaderboard_fields[6], "Hidden", 0);
259
leaderboard->hidden = (uint8_t)result;
260
261
if (!leaderboard_fields[4].value_end)
262
return RC_MISSING_VALUE;
263
len = leaderboard_fields[4].value_end - leaderboard_fields[4].value_start - 2;
264
if (len < sizeof(format) - 1) {
265
memcpy(format, leaderboard_fields[4].value_start + 1, len);
266
format[len] = '\0';
267
leaderboard->format = rc_parse_format(format);
268
}
269
else {
270
leaderboard->format = RC_FORMAT_VALUE;
271
}
272
273
++leaderboard;
274
}
275
276
return RC_OK;
277
}
278
279
int rc_api_process_fetch_game_data_server_response(rc_api_fetch_game_data_response_t* response, const rc_api_server_response_t* server_response) {
280
rc_json_field_t array_field;
281
size_t len;
282
int result;
283
284
rc_json_field_t fields[] = {
285
RC_JSON_NEW_FIELD("Success"),
286
RC_JSON_NEW_FIELD("Error"),
287
RC_JSON_NEW_FIELD("Code"),
288
RC_JSON_NEW_FIELD("PatchData") /* nested object */
289
};
290
291
rc_json_field_t patchdata_fields[] = {
292
RC_JSON_NEW_FIELD("ID"),
293
RC_JSON_NEW_FIELD("Title"),
294
RC_JSON_NEW_FIELD("ConsoleID"),
295
RC_JSON_NEW_FIELD("ImageIcon"),
296
RC_JSON_NEW_FIELD("ImageIconURL"),
297
RC_JSON_NEW_FIELD("RichPresencePatch"),
298
RC_JSON_NEW_FIELD("Achievements"), /* array */
299
RC_JSON_NEW_FIELD("Leaderboards"), /* array */
300
};
301
302
memset(response, 0, sizeof(*response));
303
rc_buffer_init(&response->response.buffer);
304
305
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
306
if (result != RC_OK || !response->response.succeeded)
307
return result;
308
309
if (!rc_json_get_required_object(patchdata_fields, sizeof(patchdata_fields) / sizeof(patchdata_fields[0]), &response->response, &fields[3], "PatchData"))
310
return RC_MISSING_VALUE;
311
312
if (!rc_json_get_required_unum(&response->id, &response->response, &patchdata_fields[0], "ID"))
313
return RC_MISSING_VALUE;
314
if (!rc_json_get_required_string(&response->title, &response->response, &patchdata_fields[1], "Title"))
315
return RC_MISSING_VALUE;
316
if (!rc_json_get_required_unum(&response->console_id, &response->response, &patchdata_fields[2], "ConsoleID"))
317
return RC_MISSING_VALUE;
318
319
/* ImageIcon will be '/Images/0123456.png' - only return the '0123456' */
320
rc_json_extract_filename(&patchdata_fields[3]);
321
rc_json_get_optional_string(&response->image_name, &response->response, &patchdata_fields[3], "ImageIcon", "");
322
rc_json_get_optional_string(&response->image_url, &response->response, &patchdata_fields[4], "ImageIconURL", "");
323
if (!response->image_url[0])
324
response->image_url = rc_api_build_avatar_url(&response->response.buffer, RC_IMAGE_TYPE_GAME, response->image_name);
325
326
/* estimate the amount of space necessary to store the rich presence script, achievements, and leaderboards.
327
determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)
328
and add space for the structures. */
329
len = patchdata_fields[5].value_end - patchdata_fields[5].value_start; /* rich presence */
330
331
len += (patchdata_fields[6].value_end - patchdata_fields[6].value_start) - /* achievements */
332
patchdata_fields[6].array_size * (80 - sizeof(rc_api_achievement_definition_t));
333
334
len += (patchdata_fields[7].value_end - patchdata_fields[7].value_start) - /* leaderboards */
335
patchdata_fields[7].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));
336
337
rc_buffer_reserve(&response->response.buffer, len);
338
/* end estimation */
339
340
rc_json_get_optional_string(&response->rich_presence_script, &response->response, &patchdata_fields[5], "RichPresencePatch", "");
341
if (!response->rich_presence_script)
342
response->rich_presence_script = "";
343
344
if (!rc_json_get_required_array(&response->num_achievements, &array_field, &response->response, &patchdata_fields[6], "Achievements"))
345
return RC_MISSING_VALUE;
346
347
if (response->num_achievements) {
348
response->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_achievements * sizeof(rc_api_achievement_definition_t));
349
if (!response->achievements)
350
return RC_OUT_OF_MEMORY;
351
352
result = rc_api_process_fetch_game_data_achievements(&response->response, response->achievements, &array_field);
353
if (result != RC_OK)
354
return result;
355
}
356
357
if (!rc_json_get_required_array(&response->num_leaderboards, &array_field, &response->response, &patchdata_fields[7], "Leaderboards"))
358
return RC_MISSING_VALUE;
359
360
if (response->num_leaderboards) {
361
response->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));
362
if (!response->leaderboards)
363
return RC_OUT_OF_MEMORY;
364
365
result = rc_api_process_fetch_game_data_leaderboards(&response->response, response->leaderboards, &array_field);
366
if (result != RC_OK)
367
return result;
368
}
369
370
return RC_OK;
371
}
372
373
void rc_api_destroy_fetch_game_data_response(rc_api_fetch_game_data_response_t* response) {
374
rc_buffer_destroy(&response->response.buffer);
375
}
376
377
/* --- Fetch Game Sets --- */
378
379
int rc_api_init_fetch_game_sets_request(rc_api_request_t* request, const rc_api_fetch_game_sets_request_t* api_params) {
380
return rc_api_init_fetch_game_sets_request_hosted(request, api_params, &g_host);
381
}
382
383
int rc_api_init_fetch_game_sets_request_hosted(rc_api_request_t* request,
384
const rc_api_fetch_game_sets_request_t* api_params,
385
const rc_api_host_t* host) {
386
rc_api_url_builder_t builder;
387
388
rc_api_url_build_dorequest_url(request, host);
389
390
if (!api_params->game_id && (!api_params->game_hash || !api_params->game_hash[0]))
391
return RC_INVALID_STATE;
392
393
rc_url_builder_init(&builder, &request->buffer, 48);
394
if (rc_api_url_build_dorequest(&builder, "achievementsets", api_params->username, api_params->api_token)) {
395
if (api_params->game_id)
396
rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
397
else
398
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
399
400
request->post_data = rc_url_builder_finalize(&builder);
401
request->content_type = RC_CONTENT_TYPE_URLENCODED;
402
}
403
404
return builder.result;
405
}
406
407
static int rc_api_process_fetch_game_sets_achievement_sets(rc_api_fetch_game_sets_response_t* response,
408
rc_api_achievement_set_definition_t* subset,
409
rc_json_field_t* subset_array_field) {
410
rc_json_iterator_t iterator;
411
rc_json_field_t array_field;
412
size_t len;
413
int result;
414
415
rc_json_field_t subset_fields[] = {
416
RC_JSON_NEW_FIELD("AchievementSetId"),
417
RC_JSON_NEW_FIELD("GameId"),
418
RC_JSON_NEW_FIELD("Title"),
419
RC_JSON_NEW_FIELD("Type"),
420
RC_JSON_NEW_FIELD("ImageIconUrl"),
421
RC_JSON_NEW_FIELD("Achievements"), /* array */
422
RC_JSON_NEW_FIELD("Leaderboards") /* array */
423
};
424
425
memset(&iterator, 0, sizeof(iterator));
426
iterator.json = subset_array_field->value_start;
427
iterator.end = subset_array_field->value_end;
428
429
while (rc_json_get_array_entry_object(subset_fields, sizeof(subset_fields) / sizeof(subset_fields[0]), &iterator)) {
430
if (!rc_json_get_required_unum(&subset->id, &response->response, &subset_fields[0], "AchievementSetId"))
431
return RC_MISSING_VALUE;
432
if (!rc_json_get_required_unum(&subset->game_id, &response->response, &subset_fields[1], "GameId"))
433
return RC_MISSING_VALUE;
434
435
if (!rc_json_get_required_string(&subset->title, &response->response, &subset_fields[2], "Title"))
436
return RC_MISSING_VALUE;
437
if (!subset->title || !subset->title[0])
438
subset->title = response->title;
439
440
if (rc_json_field_string_matches(&subset_fields[3], "core"))
441
subset->type = RC_ACHIEVEMENT_SET_TYPE_CORE;
442
else if (rc_json_field_string_matches(&subset_fields[3], "bonus"))
443
subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS;
444
else if (rc_json_field_string_matches(&subset_fields[3], "specialty"))
445
subset->type = RC_ACHIEVEMENT_SET_TYPE_SPECIALTY;
446
else if (rc_json_field_string_matches(&subset_fields[3], "exclusive"))
447
subset->type = RC_ACHIEVEMENT_SET_TYPE_EXCLUSIVE;
448
else
449
subset->type = RC_ACHIEVEMENT_SET_TYPE_BONUS;
450
451
if (rc_json_field_string_matches(&subset_fields[4], response->image_url)) {
452
subset->image_url = response->image_url;
453
subset->image_name = response->image_name;
454
}
455
else {
456
if (!rc_json_get_required_string(&subset->image_url, &response->response, &subset_fields[4], "ImageIconUrl"))
457
return RC_MISSING_VALUE;
458
rc_json_extract_filename(&subset_fields[4]);
459
rc_json_get_optional_string(&subset->image_name, &response->response, &subset_fields[4], "ImageIconUrl", "");
460
}
461
462
/* estimate the amount of space necessary to store the achievements, and leaderboards.
463
determine how much space each takes as a string in the JSON, then subtract out the non-data (field names, punctuation)
464
and add space for the structures. */
465
len = (subset_fields[5].value_end - subset_fields[5].value_start) - /* achievements */
466
subset_fields[5].array_size * (80 - sizeof(rc_api_achievement_definition_t));
467
len += (subset_fields[6].value_end - subset_fields[6].value_start) - /* leaderboards */
468
subset_fields[6].array_size * (60 - sizeof(rc_api_leaderboard_definition_t));
469
470
rc_buffer_reserve(&response->response.buffer, len);
471
/* end estimation */
472
473
if (!rc_json_get_required_array(&subset->num_achievements, &array_field, &response->response, &subset_fields[5], "Achievements"))
474
return RC_MISSING_VALUE;
475
476
if (subset->num_achievements) {
477
subset->achievements = (rc_api_achievement_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_achievements * sizeof(rc_api_achievement_definition_t));
478
if (!subset->achievements)
479
return RC_OUT_OF_MEMORY;
480
481
result = rc_api_process_fetch_game_data_achievements(&response->response, subset->achievements, &array_field);
482
if (result != RC_OK)
483
return result;
484
}
485
486
if (!rc_json_get_required_array(&subset->num_leaderboards, &array_field, &response->response, &subset_fields[6], "Leaderboards"))
487
return RC_MISSING_VALUE;
488
489
if (subset->num_leaderboards) {
490
subset->leaderboards = (rc_api_leaderboard_definition_t*)rc_buffer_alloc(&response->response.buffer, subset->num_leaderboards * sizeof(rc_api_leaderboard_definition_t));
491
if (!subset->leaderboards)
492
return RC_OUT_OF_MEMORY;
493
494
result = rc_api_process_fetch_game_data_leaderboards(&response->response, subset->leaderboards, &array_field);
495
if (result != RC_OK)
496
return result;
497
}
498
499
++subset;
500
}
501
502
return RC_OK;
503
}
504
505
int rc_api_process_fetch_game_sets_server_response(rc_api_fetch_game_sets_response_t* response, const rc_api_server_response_t* server_response) {
506
rc_json_field_t array_field;
507
int result;
508
509
rc_json_field_t fields[] = {
510
RC_JSON_NEW_FIELD("Success"),
511
RC_JSON_NEW_FIELD("Error"),
512
RC_JSON_NEW_FIELD("Code"),
513
RC_JSON_NEW_FIELD("GameId"),
514
RC_JSON_NEW_FIELD("Title"),
515
RC_JSON_NEW_FIELD("ConsoleId"),
516
RC_JSON_NEW_FIELD("ImageIconUrl"),
517
RC_JSON_NEW_FIELD("RichPresenceGameId"),
518
RC_JSON_NEW_FIELD("RichPresencePatch"),
519
RC_JSON_NEW_FIELD("Sets") /* array */
520
};
521
522
memset(response, 0, sizeof(*response));
523
rc_buffer_init(&response->response.buffer);
524
525
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
526
if (result != RC_OK || !response->response.succeeded)
527
return result;
528
529
if (!rc_json_get_required_unum(&response->id, &response->response, &fields[3], "GameId"))
530
return RC_MISSING_VALUE;
531
if (!rc_json_get_required_string(&response->title, &response->response, &fields[4], "Title"))
532
return RC_MISSING_VALUE;
533
if (!rc_json_get_required_unum(&response->console_id, &response->response, &fields[5], "ConsoleId"))
534
return RC_MISSING_VALUE;
535
536
rc_json_get_required_string(&response->image_url, &response->response, &fields[6], "ImageIconUrl");
537
rc_json_extract_filename(&fields[6]);
538
rc_json_get_required_string(&response->image_name, &response->response, &fields[6], "ImageIconUrl");
539
540
rc_json_get_optional_unum(&response->session_game_id, &fields[7], "RichPresenceGameId", response->id);
541
542
rc_json_get_optional_string(&response->rich_presence_script, &response->response, &fields[8], "RichPresencePatch", "");
543
if (!response->rich_presence_script)
544
response->rich_presence_script = "";
545
546
rc_json_get_optional_array(&response->num_sets, &array_field, &fields[9], "Sets");
547
if (response->num_sets) {
548
response->sets = (rc_api_achievement_set_definition_t*)rc_buffer_alloc(&response->response.buffer, response->num_sets * sizeof(rc_api_achievement_set_definition_t));
549
if (!response->sets)
550
return RC_OUT_OF_MEMORY;
551
552
result = rc_api_process_fetch_game_sets_achievement_sets(response, response->sets, &array_field);
553
if (result != RC_OK)
554
return result;
555
}
556
557
return RC_OK;
558
}
559
560
void rc_api_destroy_fetch_game_sets_response(rc_api_fetch_game_sets_response_t* response) {
561
rc_buffer_destroy(&response->response.buffer);
562
}
563
564
/* --- Ping --- */
565
566
int rc_api_init_ping_request(rc_api_request_t* request, const rc_api_ping_request_t* api_params) {
567
return rc_api_init_ping_request_hosted(request, api_params, &g_host);
568
}
569
570
int rc_api_init_ping_request_hosted(rc_api_request_t* request,
571
const rc_api_ping_request_t* api_params,
572
const rc_api_host_t* host) {
573
rc_api_url_builder_t builder;
574
575
rc_api_url_build_dorequest_url(request, host);
576
577
if (api_params->game_id == 0)
578
return RC_INVALID_STATE;
579
580
rc_url_builder_init(&builder, &request->buffer, 48);
581
if (rc_api_url_build_dorequest(&builder, "ping", api_params->username, api_params->api_token)) {
582
rc_url_builder_append_unum_param(&builder, "g", api_params->game_id);
583
584
if (api_params->rich_presence && *api_params->rich_presence)
585
rc_url_builder_append_str_param(&builder, "m", api_params->rich_presence);
586
587
if (api_params->game_hash && *api_params->game_hash) {
588
rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore);
589
rc_url_builder_append_str_param(&builder, "x", api_params->game_hash);
590
}
591
592
request->post_data = rc_url_builder_finalize(&builder);
593
request->content_type = RC_CONTENT_TYPE_URLENCODED;
594
}
595
596
return builder.result;
597
}
598
599
int rc_api_process_ping_response(rc_api_ping_response_t* response, const char* server_response) {
600
rc_api_server_response_t response_obj;
601
602
memset(&response_obj, 0, sizeof(response_obj));
603
response_obj.body = server_response;
604
response_obj.body_length = rc_json_get_object_string_length(server_response);
605
606
return rc_api_process_ping_server_response(response, &response_obj);
607
}
608
609
int rc_api_process_ping_server_response(rc_api_ping_response_t* response, const rc_api_server_response_t* server_response) {
610
rc_json_field_t fields[] = {
611
RC_JSON_NEW_FIELD("Success"),
612
RC_JSON_NEW_FIELD("Error")
613
};
614
615
memset(response, 0, sizeof(*response));
616
rc_buffer_init(&response->response.buffer);
617
618
return rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
619
}
620
621
void rc_api_destroy_ping_response(rc_api_ping_response_t* response) {
622
rc_buffer_destroy(&response->response.buffer);
623
}
624
625
/* --- Award Achievement --- */
626
627
int rc_api_init_award_achievement_request(rc_api_request_t* request, const rc_api_award_achievement_request_t* api_params) {
628
return rc_api_init_award_achievement_request_hosted(request, api_params, &g_host);
629
}
630
631
int rc_api_init_award_achievement_request_hosted(rc_api_request_t* request,
632
const rc_api_award_achievement_request_t* api_params,
633
const rc_api_host_t* host) {
634
rc_api_url_builder_t builder;
635
char buffer[33];
636
md5_state_t md5;
637
md5_byte_t digest[16];
638
639
rc_api_url_build_dorequest_url(request, host);
640
641
if (api_params->achievement_id == 0)
642
return RC_INVALID_STATE;
643
644
rc_url_builder_init(&builder, &request->buffer, 96);
645
if (rc_api_url_build_dorequest(&builder, "awardachievement", api_params->username, api_params->api_token)) {
646
rc_url_builder_append_unum_param(&builder, "a", api_params->achievement_id);
647
rc_url_builder_append_unum_param(&builder, "h", api_params->hardcore ? 1 : 0);
648
if (api_params->game_hash && *api_params->game_hash)
649
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
650
if (api_params->seconds_since_unlock)
651
rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_unlock);
652
653
/* Evaluate the signature. */
654
md5_init(&md5);
655
snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);
656
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
657
md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
658
snprintf(buffer, sizeof(buffer), "%d", api_params->hardcore ? 1 : 0);
659
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
660
if (api_params->seconds_since_unlock) {
661
/* second achievement id is needed by delegated unlock. including it here allows overloading
662
* the hash generating code on the server */
663
snprintf(buffer, sizeof(buffer), "%u", api_params->achievement_id);
664
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
665
snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_unlock);
666
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
667
}
668
md5_finish(&md5, digest);
669
rc_format_md5(buffer, digest);
670
rc_url_builder_append_str_param(&builder, "v", buffer);
671
672
request->post_data = rc_url_builder_finalize(&builder);
673
request->content_type = RC_CONTENT_TYPE_URLENCODED;
674
}
675
676
return builder.result;
677
}
678
679
int rc_api_process_award_achievement_response(rc_api_award_achievement_response_t* response, const char* server_response) {
680
rc_api_server_response_t response_obj;
681
682
memset(&response_obj, 0, sizeof(response_obj));
683
response_obj.body = server_response;
684
response_obj.body_length = rc_json_get_object_string_length(server_response);
685
686
return rc_api_process_award_achievement_server_response(response, &response_obj);
687
}
688
689
int rc_api_process_award_achievement_server_response(rc_api_award_achievement_response_t* response, const rc_api_server_response_t* server_response) {
690
int result;
691
rc_json_field_t fields[] = {
692
RC_JSON_NEW_FIELD("Success"),
693
RC_JSON_NEW_FIELD("Error"),
694
RC_JSON_NEW_FIELD("Score"),
695
RC_JSON_NEW_FIELD("SoftcoreScore"),
696
RC_JSON_NEW_FIELD("AchievementID"),
697
RC_JSON_NEW_FIELD("AchievementsRemaining")
698
};
699
700
memset(response, 0, sizeof(*response));
701
rc_buffer_init(&response->response.buffer);
702
703
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
704
if (result != RC_OK)
705
return result;
706
707
if (!response->response.succeeded) {
708
if (response->response.error_message &&
709
memcmp(response->response.error_message, "User already has", 16) == 0) {
710
/* not really an error, the achievement is unlocked, just not by the current call.
711
* hardcore: User already has hardcore and regular achievements awarded.
712
* non-hardcore: User already has this achievement awarded.
713
*/
714
response->response.succeeded = 1;
715
} else {
716
return result;
717
}
718
}
719
720
rc_json_get_optional_unum(&response->new_player_score, &fields[2], "Score", 0);
721
rc_json_get_optional_unum(&response->new_player_score_softcore, &fields[3], "SoftcoreScore", 0);
722
rc_json_get_optional_unum(&response->awarded_achievement_id, &fields[4], "AchievementID", 0);
723
rc_json_get_optional_unum(&response->achievements_remaining, &fields[5], "AchievementsRemaining", (unsigned)-1);
724
725
return RC_OK;
726
}
727
728
void rc_api_destroy_award_achievement_response(rc_api_award_achievement_response_t* response) {
729
rc_buffer_destroy(&response->response.buffer);
730
}
731
732
/* --- Submit Leaderboard Entry --- */
733
734
int rc_api_init_submit_lboard_entry_request(rc_api_request_t* request, const rc_api_submit_lboard_entry_request_t* api_params) {
735
return rc_api_init_submit_lboard_entry_request_hosted(request, api_params, &g_host);
736
}
737
738
int rc_api_init_submit_lboard_entry_request_hosted(rc_api_request_t* request,
739
const rc_api_submit_lboard_entry_request_t* api_params,
740
const rc_api_host_t* host) {
741
rc_api_url_builder_t builder;
742
char buffer[33];
743
md5_state_t md5;
744
md5_byte_t digest[16];
745
746
rc_api_url_build_dorequest_url(request, host);
747
748
if (api_params->leaderboard_id == 0)
749
return RC_INVALID_STATE;
750
751
rc_url_builder_init(&builder, &request->buffer, 96);
752
if (rc_api_url_build_dorequest(&builder, "submitlbentry", api_params->username, api_params->api_token)) {
753
rc_url_builder_append_unum_param(&builder, "i", api_params->leaderboard_id);
754
rc_url_builder_append_num_param(&builder, "s", api_params->score);
755
756
if (api_params->game_hash && *api_params->game_hash)
757
rc_url_builder_append_str_param(&builder, "m", api_params->game_hash);
758
759
if (api_params->seconds_since_completion)
760
rc_url_builder_append_unum_param(&builder, "o", api_params->seconds_since_completion);
761
762
/* Evaluate the signature. */
763
md5_init(&md5);
764
snprintf(buffer, sizeof(buffer), "%u", api_params->leaderboard_id);
765
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
766
md5_append(&md5, (md5_byte_t*)api_params->username, (int)strlen(api_params->username));
767
snprintf(buffer, sizeof(buffer), "%d", api_params->score);
768
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
769
if (api_params->seconds_since_completion) {
770
snprintf(buffer, sizeof(buffer), "%u", api_params->seconds_since_completion);
771
md5_append(&md5, (md5_byte_t*)buffer, (int)strlen(buffer));
772
}
773
md5_finish(&md5, digest);
774
rc_format_md5(buffer, digest);
775
rc_url_builder_append_str_param(&builder, "v", buffer);
776
777
request->post_data = rc_url_builder_finalize(&builder);
778
request->content_type = RC_CONTENT_TYPE_URLENCODED;
779
}
780
781
return builder.result;
782
}
783
784
int rc_api_process_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response, const char* server_response) {
785
rc_api_server_response_t response_obj;
786
787
memset(&response_obj, 0, sizeof(response_obj));
788
response_obj.body = server_response;
789
response_obj.body_length = rc_json_get_object_string_length(server_response);
790
791
return rc_api_process_submit_lboard_entry_server_response(response, &response_obj);
792
}
793
794
int rc_api_process_submit_lboard_entry_server_response(rc_api_submit_lboard_entry_response_t* response, const rc_api_server_response_t* server_response) {
795
rc_api_lboard_entry_t* entry;
796
rc_json_field_t array_field;
797
rc_json_iterator_t iterator;
798
const char* str;
799
int result;
800
801
rc_json_field_t fields[] = {
802
RC_JSON_NEW_FIELD("Success"),
803
RC_JSON_NEW_FIELD("Error"),
804
RC_JSON_NEW_FIELD("Response") /* nested object */
805
};
806
807
rc_json_field_t response_fields[] = {
808
RC_JSON_NEW_FIELD("Score"),
809
RC_JSON_NEW_FIELD("BestScore"),
810
RC_JSON_NEW_FIELD("RankInfo"), /* nested object */
811
RC_JSON_NEW_FIELD("TopEntries") /* array */
812
/* unused fields
813
RC_JSON_NEW_FIELD("LBData"), / * array * /
814
RC_JSON_NEW_FIELD("ScoreFormatted"),
815
RC_JSON_NEW_FIELD("TopEntriesFriends") / * array * /
816
* unused fields */
817
};
818
819
/* unused fields
820
rc_json_field_t lbdata_fields[] = {
821
RC_JSON_NEW_FIELD("Format"),
822
RC_JSON_NEW_FIELD("LeaderboardID"),
823
RC_JSON_NEW_FIELD("GameID"),
824
RC_JSON_NEW_FIELD("Title"),
825
RC_JSON_NEW_FIELD("LowerIsBetter")
826
};
827
* unused fields */
828
829
rc_json_field_t entry_fields[] = {
830
RC_JSON_NEW_FIELD("User"),
831
RC_JSON_NEW_FIELD("Rank"),
832
RC_JSON_NEW_FIELD("Score")
833
/* unused fields
834
RC_JSON_NEW_FIELD("DateSubmitted")
835
* unused fields */
836
};
837
838
rc_json_field_t rank_info_fields[] = {
839
RC_JSON_NEW_FIELD("Rank"),
840
RC_JSON_NEW_FIELD("NumEntries")
841
/* unused fields
842
RC_JSON_NEW_FIELD("LowerIsBetter"),
843
RC_JSON_NEW_FIELD("UserRank")
844
* unused fields */
845
};
846
847
memset(response, 0, sizeof(*response));
848
rc_buffer_init(&response->response.buffer);
849
850
result = rc_json_parse_server_response(&response->response, server_response, fields, sizeof(fields) / sizeof(fields[0]));
851
if (result != RC_OK || !response->response.succeeded)
852
return result;
853
854
if (!rc_json_get_required_object(response_fields, sizeof(response_fields) / sizeof(response_fields[0]), &response->response, &fields[2], "Response"))
855
return RC_MISSING_VALUE;
856
if (!rc_json_get_required_num(&response->submitted_score, &response->response, &response_fields[0], "Score"))
857
return RC_MISSING_VALUE;
858
if (!rc_json_get_required_num(&response->best_score, &response->response, &response_fields[1], "BestScore"))
859
return RC_MISSING_VALUE;
860
861
if (!rc_json_get_required_object(rank_info_fields, sizeof(rank_info_fields) / sizeof(rank_info_fields[0]), &response->response, &response_fields[2], "RankInfo"))
862
return RC_MISSING_VALUE;
863
if (!rc_json_get_required_unum(&response->new_rank, &response->response, &rank_info_fields[0], "Rank"))
864
return RC_MISSING_VALUE;
865
if (!rc_json_get_required_string(&str, &response->response, &rank_info_fields[1], "NumEntries"))
866
return RC_MISSING_VALUE;
867
response->num_entries = (unsigned)atoi(str);
868
869
if (!rc_json_get_required_array(&response->num_top_entries, &array_field, &response->response, &response_fields[3], "TopEntries"))
870
return RC_MISSING_VALUE;
871
872
if (response->num_top_entries) {
873
response->top_entries = (rc_api_lboard_entry_t*)rc_buffer_alloc(&response->response.buffer, response->num_top_entries * sizeof(rc_api_lboard_entry_t));
874
if (!response->top_entries)
875
return RC_OUT_OF_MEMORY;
876
877
memset(&iterator, 0, sizeof(iterator));
878
iterator.json = array_field.value_start;
879
iterator.end = array_field.value_end;
880
881
entry = response->top_entries;
882
while (rc_json_get_array_entry_object(entry_fields, sizeof(entry_fields) / sizeof(entry_fields[0]), &iterator)) {
883
if (!rc_json_get_required_string(&entry->username, &response->response, &entry_fields[0], "User"))
884
return RC_MISSING_VALUE;
885
886
if (!rc_json_get_required_unum(&entry->rank, &response->response, &entry_fields[1], "Rank"))
887
return RC_MISSING_VALUE;
888
889
if (!rc_json_get_required_num(&entry->score, &response->response, &entry_fields[2], "Score"))
890
return RC_MISSING_VALUE;
891
892
++entry;
893
}
894
}
895
896
return RC_OK;
897
}
898
899
void rc_api_destroy_submit_lboard_entry_response(rc_api_submit_lboard_entry_response_t* response) {
900
rc_buffer_destroy(&response->response.buffer);
901
}
902
903