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